mirror of https://github.com/alibaba/ice.git
				
				
				
			feat: support swc options for plugin-rax-compat and onappear on rax-compat (#349)
* feat: support swc options for plugin-rax-compat * chore: example * test: fix test case * test: fix test case * test: fix test case * fix: outputdir * fix: onappear for no ref cases * fix: merge options * fix: appear polyfill * chore: remove appear polyfill dependency * fix: add swc helpers * fix: onappear on rax-compat * refactor: format code and not to use react.frowardRef Co-authored-by: ZeroLing <zhuoling.lcl@alibaba-inc.com>
This commit is contained in:
		
							parent
							
								
									064e290ec6
								
							
						
					
					
						commit
						48603cdf8f
					
				|  | @ -1,7 +1,6 @@ | |||
| import { defineConfig } from '@ice/app'; | ||||
| import SpeedMeasurePlugin from 'speed-measure-webpack-plugin'; | ||||
| import auth from '@ice/plugin-auth'; | ||||
| import compatRax from '@ice/plugin-rax-compat'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   publicPath: '/', | ||||
|  | @ -22,6 +21,6 @@ export default defineConfig({ | |||
|     return webpackConfig; | ||||
|   }, | ||||
|   dropLogLevel: 'warn', | ||||
|   plugins: [auth(), compatRax()], | ||||
|   plugins: [auth()], | ||||
|   eslint: true, | ||||
| }); | ||||
|  |  | |||
|  | @ -14,11 +14,6 @@ | |||
|     "@ice/plugin-rax-compat": "workspace:*", | ||||
|     "@ice/runtime": "workspace:*", | ||||
|     "ahooks": "^3.3.8", | ||||
|     "rax": "^1.2.2", | ||||
|     "rax-image": "^2.4.1", | ||||
|     "rax-is-valid-element": "^1.0.0", | ||||
|     "rax-text": "^2.2.0", | ||||
|     "rax-view": "^2.3.0", | ||||
|     "react": "^18.0.0", | ||||
|     "react-dom": "^18.0.0" | ||||
|   }, | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| chrome 55 | ||||
|  | @ -0,0 +1,9 @@ | |||
| import { defineConfig } from '@ice/app'; | ||||
| import compatRax from '@ice/plugin-rax-compat'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   ssr: false, | ||||
|   ssg: false, | ||||
|   publicPath: '/', | ||||
|   plugins: [compatRax()], | ||||
| }); | ||||
|  | @ -0,0 +1,30 @@ | |||
| { | ||||
|   "name": "rax-project", | ||||
|   "version": "1.0.0", | ||||
|   "scripts": { | ||||
|     "start": "ice start", | ||||
|     "build": "ice build" | ||||
|   }, | ||||
|   "description": "", | ||||
|   "author": "", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@ice/app": "workspace:*", | ||||
|     "@ice/plugin-rax-compat": "workspace:*", | ||||
|     "@ice/runtime": "workspace:*", | ||||
|     "rax": "^1.2.2", | ||||
|     "rax-image": "^2.4.1", | ||||
|     "rax-is-valid-element": "^1.0.0", | ||||
|     "rax-text": "^2.2.0", | ||||
|     "rax-view": "^2.3.0", | ||||
|     "react": "^18.0.0", | ||||
|     "react-dom": "^18.0.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/react": "^18.0.0", | ||||
|     "@types/react-dom": "^18.0.2", | ||||
|     "browserslist": "^4.19.3", | ||||
|     "regenerator-runtime": "^0.13.9", | ||||
|     "webpack": "^5.73.0" | ||||
|   } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.7 KiB | 
|  | @ -0,0 +1,7 @@ | |||
| import { defineAppConfig } from 'ice'; | ||||
| 
 | ||||
| export default defineAppConfig({ | ||||
|   app: { | ||||
|     rootId: 'app', | ||||
|   }, | ||||
| }); | ||||
|  | @ -0,0 +1,22 @@ | |||
| import { Meta, Title, Links, Main, Scripts } from 'ice'; | ||||
| 
 | ||||
| function Document() { | ||||
|   return ( | ||||
|     <html> | ||||
|       <head> | ||||
|         <meta charSet="utf-8" /> | ||||
|         <meta name="description" content="ICE 3.0 Demo" /> | ||||
|         <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover" /> | ||||
|         <Meta /> | ||||
|         <Title /> | ||||
|         <Links /> | ||||
|       </head> | ||||
|       <body> | ||||
|         <Main /> | ||||
|         <Scripts /> | ||||
|       </body> | ||||
|     </html> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default Document; | ||||
|  | @ -0,0 +1,3 @@ | |||
| body { | ||||
|   font-size: 14px; | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| .title { | ||||
|   color: red; | ||||
|   margin-left: 10rpx; | ||||
| } | ||||
| 
 | ||||
| .data { | ||||
|   margin-top: 10px; | ||||
| } | ||||
| 
 | ||||
| .homeContainer { | ||||
|   align-items: center; | ||||
|   margin-top: 200rpx; | ||||
| } | ||||
| 
 | ||||
| .homeTitle { | ||||
|   font-size: 45rpx; | ||||
|   font-weight: bold; | ||||
|   margin: 20rpx 0; | ||||
| } | ||||
| 
 | ||||
| .homeInfo { | ||||
|   font-size: 36rpx; | ||||
|   margin: 8rpx 0; | ||||
|   color: #555; | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| declare module '*.module.less' { | ||||
|   const classes: { [key: string]: string }; | ||||
|   export default classes; | ||||
| } | ||||
| 
 | ||||
| declare module '*.module.css' { | ||||
|   const classes: { [key: string]: string }; | ||||
|   export default classes; | ||||
| } | ||||
| 
 | ||||
| declare module '*.module.scss' { | ||||
|   const classes: { [key: string]: string }; | ||||
|   export default classes; | ||||
| } | ||||
|  | @ -0,0 +1,32 @@ | |||
| { | ||||
|   "compileOnSave": false, | ||||
|   "buildOnSave": false, | ||||
|   "compilerOptions": { | ||||
|     "baseUrl": ".", | ||||
|     "outDir": "build", | ||||
|     "module": "esnext", | ||||
|     "target": "es6", | ||||
|     "jsx": "react-jsx", | ||||
|     "moduleResolution": "node", | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "lib": ["es6", "dom"], | ||||
|     "sourceMap": true, | ||||
|     "allowJs": true, | ||||
|     "rootDir": "./", | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|     "noImplicitReturns": true, | ||||
|     "noImplicitThis": true, | ||||
|     "noImplicitAny": false, | ||||
|     "importHelpers": true, | ||||
|     "strictNullChecks": true, | ||||
|     "suppressImplicitAnyIndexErrors": true, | ||||
|     "noUnusedLocals": true, | ||||
|     "skipLibCheck": true, | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"], | ||||
|       "ice": [".ice"] | ||||
|     } | ||||
|   }, | ||||
|   "include": ["src", ".ice", "ice.config.*"], | ||||
|   "exclude": ["node_modules", "build", "public"] | ||||
| } | ||||
|  | @ -43,7 +43,7 @@ const plugin: Plugin<PluginOptions> = ({ onGetConfig, onHook, context, generator | |||
|   // Get server compiler by hooks
 | ||||
|   onHook(`before.${command as 'start' | 'build'}.run`, async ({ serverCompiler, taskConfigs, urls }) => { | ||||
|     const taskConfig = taskConfigs.find(({ name }) => name === 'web').config; | ||||
|     outputDir = taskConfig.outputDir; | ||||
|     outputDir = path.isAbsolute(taskConfig.outputDir) ? taskConfig.outputDir : path.join(rootDir, taskConfig.outputDir); | ||||
| 
 | ||||
|     // Need absolute path for pha dev.
 | ||||
|     publicPath = command === 'start' ? getDevPath(urls.lanUrlForTerminal) : (taskConfig.publicPath || '/'); | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
|   ], | ||||
|   "dependencies": { | ||||
|     "consola": "^2.15.3", | ||||
|     "lodash.merge": "^4.6.2", | ||||
|     "rax-compat": "^0.1.0", | ||||
|     "stylesheet-loader": "^0.9.1" | ||||
|   }, | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { createRequire } from 'module'; | |||
| import type { Plugin } from '@ice/types'; | ||||
| import type { RuleSetRule } from 'webpack'; | ||||
| import consola from 'consola'; | ||||
| import merge from 'lodash.merge'; | ||||
| 
 | ||||
| const require = createRequire(import.meta.url); | ||||
| 
 | ||||
|  | @ -33,6 +34,20 @@ let warnOnce = false; | |||
| function getPlugin(options: CompatRaxOptions): Plugin { | ||||
|   return ({ onGetConfig }) => { | ||||
|     onGetConfig((config) => { | ||||
|       // Reset jsc.transform.react.runtime to classic.
 | ||||
|       config.swcOptions = merge(config.swcOptions || {}, { | ||||
|         compilationConfig: { | ||||
|           jsc: { | ||||
|             transform: { | ||||
|               react: { | ||||
|                 runtime: 'classic', | ||||
|                 pragma: 'createElement', | ||||
|                 pragmaFrag: 'Fragment', | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|       Object.assign(config.alias, alias); | ||||
|       if (options.inlineStyle) { | ||||
|         if (!warnOnce) { | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
|     "compat" | ||||
|   ], | ||||
|   "dependencies": { | ||||
|     "appear-polyfill": "^0.1.2", | ||||
|     "@swc/helpers": "^0.4.3", | ||||
|     "style-unit": "^3.0.4", | ||||
|     "create-react-class": "^15.7.0" | ||||
|   }, | ||||
|  |  | |||
|  | @ -5,19 +5,11 @@ import type { | |||
|   ReactNode, | ||||
|   RefObject, | ||||
| } from 'react'; | ||||
| import { createElement as _createElement, useEffect, forwardRef } from 'react'; | ||||
| import { setupAppear } from 'appear-polyfill'; | ||||
| import { createElement as _createElement, useEffect, useCallback, useRef } from 'react'; | ||||
| import { cached, convertUnit } from 'style-unit'; | ||||
| import { observerElement } from './visibility'; | ||||
| import { isFunction, isObject, isNumber } from './type'; | ||||
| 
 | ||||
| let appearSetup = false; | ||||
| function setupAppearOnce() { | ||||
|   if (!appearSetup) { | ||||
|     setupAppear(); | ||||
|     appearSetup = true; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // https://github.com/alibaba/rax/blob/master/packages/driver-dom/src/index.js
 | ||||
| // opacity -> opa
 | ||||
| // fontWeight -> ntw
 | ||||
|  | @ -34,7 +26,6 @@ function setupAppearOnce() { | |||
| // borderImageOutset|borderImageSlice|borderImageWidth -> erim
 | ||||
| const NON_DIMENSIONAL_REG = /opa|ntw|ne[ch]|ex(?:s|g|n|p|$)|^ord|zoo|grid|orp|ows|mnc|^columns$|bs|erim|onit/i; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Compat createElement for rax export. | ||||
|  * Reference: https://github.com/alibaba/rax/blob/master/packages/rax/src/createElement.js#L13
 | ||||
|  | @ -53,8 +44,11 @@ export function createElement<P extends { | |||
|   type: FunctionComponent<P> | string, | ||||
|   props?: Attributes & P | null, | ||||
|   ...children: ReactNode[]): ReactElement { | ||||
|   // Get a shallow copy of props, to avoid mutating the original object.
 | ||||
|   const rest = Object.assign({}, props); | ||||
|   const { onAppear, onDisappear } = rest; | ||||
| 
 | ||||
|   // Delete props that are not allowed in react.
 | ||||
|   delete rest.onAppear; | ||||
|   delete rest.onDisappear; | ||||
| 
 | ||||
|  | @ -64,21 +58,53 @@ export function createElement<P extends { | |||
|     rest.style = compatStyleProps; | ||||
|   } | ||||
| 
 | ||||
|   // Create backend element.
 | ||||
|   const args = [type, rest]; | ||||
|   let element: any = _createElement.apply(null, args.concat(children as any)); | ||||
| 
 | ||||
|   // Polyfill for appear and disappear event.
 | ||||
|   // Compat for visibility events.
 | ||||
|   if (isFunction(onAppear) || isFunction(onDisappear)) { | ||||
|     setupAppearOnce(); | ||||
|     element = _createElement(forwardRef(AppearOrDisappear), { | ||||
|       onAppear: onAppear, | ||||
|       onDisappear: onDisappear, | ||||
|       ref: rest.ref, | ||||
|     }, element); | ||||
|     return _createElement( | ||||
|       VisibilityChange, | ||||
|       { | ||||
|         onAppear, | ||||
|         onDisappear, | ||||
|         // Passing child ref to `VisibilityChange` to avoid creating a new ref.
 | ||||
|         childRef: rest.ref, | ||||
|         // Using forwardedRef as a prop to the backend react element.
 | ||||
|         forwardRef: (ref: RefObject<any>) => _createElement(type, Object.assign({ ref }, rest), ...children), | ||||
|       }, | ||||
|     ); | ||||
|   } else { | ||||
|     return _createElement(type, rest, ...children); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   return element; | ||||
| function VisibilityChange({ | ||||
|   onAppear, | ||||
|   onDisappear, | ||||
|   childRef, | ||||
|   forwardRef, | ||||
| }: any) { | ||||
|   const fallbackRef = useRef(null); // `fallbackRef` used if `childRef` is not provided.
 | ||||
|   const ref = childRef || fallbackRef; | ||||
| 
 | ||||
|   const listen = useCallback((eventName: string, handler: Function) => { | ||||
|     const { current } = ref; | ||||
|     if (current != null) { | ||||
|       if (isFunction(handler)) { | ||||
|         observerElement(current as HTMLElement); | ||||
|         current.addEventListener(eventName, handler); | ||||
|       } | ||||
|     } | ||||
|     return () => { | ||||
|       const { current } = ref; | ||||
|       if (current) { | ||||
|         current.removeEventListener(eventName, handler); | ||||
|       } | ||||
|     }; | ||||
|   }, [ref]); | ||||
| 
 | ||||
|   useEffect(() => listen('appear', onAppear), [ref, onAppear, listen]); | ||||
|   useEffect(() => listen('disappear', onDisappear), [ref, onDisappear, listen]); | ||||
| 
 | ||||
|   return forwardRef(ref); | ||||
| } | ||||
| 
 | ||||
| const isDimensionalProp = cached((prop: string) => !NON_DIMENSIONAL_REG.test(prop)); | ||||
|  | @ -102,31 +128,3 @@ function compatStyle<S = object>(style?: S): S | void { | |||
|   } | ||||
|   return style; | ||||
| } | ||||
| 
 | ||||
| // Appear HOC Component.
 | ||||
| function AppearOrDisappear(props: any, ref: RefObject<EventTarget>) { | ||||
|   const { onAppear, onDisappear } = props; | ||||
| 
 | ||||
|   listen('appear', onAppear); | ||||
|   listen('disappear', onDisappear); | ||||
| 
 | ||||
|   function listen(eventName: string, handler: EventListenerOrEventListenerObject) { | ||||
|     if (isFunction(handler) && ref) { | ||||
|       // eslint-disable-next-line react-hooks/rules-of-hooks
 | ||||
|       useEffect(() => { | ||||
|         const { current } = ref; | ||||
|         if (current != null) { | ||||
|           current.addEventListener(eventName, handler); | ||||
|         } | ||||
|         return () => { | ||||
|           const { current } = ref; | ||||
|           if (current) { | ||||
|             current.removeEventListener(eventName, handler); | ||||
|           } | ||||
|         }; | ||||
|       }, [ref, handler]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return props.children; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,652 @@ | |||
| /** | ||||
|  * An IntersectionObserver registry. This registry exists to hold a strong | ||||
|  * reference to IntersectionObserver instances currently observing a target | ||||
|  * element. Without this registry, instances without another reference may be | ||||
|  * garbage collected. | ||||
|  */ | ||||
| const registry = []; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Creates the global IntersectionObserverEntry constructor. | ||||
|  * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
 | ||||
|  * @param {Object} entry A dictionary of instance properties. | ||||
|  * @constructor | ||||
|  */ | ||||
| export function IntersectionObserverEntry(entry) { | ||||
|   this.time = entry.time; | ||||
|   this.target = entry.target; | ||||
|   this.rootBounds = entry.rootBounds; | ||||
|   this.boundingClientRect = entry.boundingClientRect; | ||||
|   this.intersectionRect = entry.intersectionRect || getEmptyRect(); | ||||
|   this.isIntersecting = !!entry.intersectionRect; | ||||
| 
 | ||||
|   // Calculates the intersection ratio.
 | ||||
|   const targetRect = this.boundingClientRect; | ||||
|   const targetArea = targetRect.width * targetRect.height; | ||||
|   const { intersectionRect } = this; | ||||
|   const intersectionArea = intersectionRect.width * intersectionRect.height; | ||||
| 
 | ||||
|   // Sets intersection ratio.
 | ||||
|   if (targetArea) { | ||||
|     // Round the intersection ratio to avoid floating point math issues:
 | ||||
|     // https://github.com/w3c/IntersectionObserver/issues/324
 | ||||
|     this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4)); | ||||
|   } else { | ||||
|     // If area is zero and is intersecting, sets to 1, otherwise to 0
 | ||||
|     this.intersectionRatio = this.isIntersecting ? 1 : 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default class IntersectionObserver { | ||||
|   /** | ||||
|    * The minimum interval within which the document will be checked for | ||||
|    * intersection changes. | ||||
|    */ | ||||
|   THROTTLE_TIMEOUT = 100; | ||||
| 
 | ||||
|   /** | ||||
|    * The frequency in which the polyfill polls for intersection changes. | ||||
|    * this can be updated on a per instance basis and must be set prior to | ||||
|    * calling `observe` on the first target. | ||||
|    */ | ||||
|   POLL_INTERVAL = null; | ||||
| 
 | ||||
|   /** | ||||
|    * Use a mutation observer on the root element | ||||
|    * to detect intersection changes. | ||||
|    */ | ||||
|   USE_MUTATION_OBSERVER = true; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the global IntersectionObserver constructor. | ||||
|    * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
 | ||||
|    * @param {Function} callback The function to be invoked after intersection | ||||
|    *     changes have queued. The function is not invoked if the queue has | ||||
|    *     been emptied by calling the `takeRecords` method. | ||||
|    * @param {Object=} optOptions Optional configuration options. | ||||
|    * @constructor | ||||
|    */ | ||||
|   constructor(callback, optOptions) { | ||||
|     const options = optOptions || {}; | ||||
| 
 | ||||
|     if (typeof callback != 'function') { | ||||
|       throw new Error('callback must be a function'); | ||||
|     } | ||||
| 
 | ||||
|     if (options.root && options.root.nodeType != 1) { | ||||
|       throw new Error('root must be an Element'); | ||||
|     } | ||||
| 
 | ||||
|     // Throttles `this._checkForIntersections`.
 | ||||
|     this._checkForIntersections = throttle(this._checkForIntersections, this.THROTTLE_TIMEOUT); | ||||
| 
 | ||||
|     // Private properties.
 | ||||
|     this._callback = callback; | ||||
|     this._observationTargets = []; | ||||
|     this._queuedEntries = []; | ||||
|     this._rootMarginValues = this._parseRootMargin(options.rootMargin); | ||||
| 
 | ||||
|     // Public properties.
 | ||||
|     this.thresholds = this._initThresholds(options.threshold); | ||||
|     this.root = options.root || null; | ||||
|     this.rootMargin = this._rootMarginValues.map((margin) => margin.value + margin.unit).join(' '); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Starts observing a target element for intersection changes based on | ||||
|    * the thresholds values. | ||||
|    * @param {Element} target The DOM element to observe. | ||||
|    */ | ||||
|   observe(target) { | ||||
|     const isTargetAlreadyObserved = this._observationTargets.some((item) => item.element === target); | ||||
| 
 | ||||
|     if (isTargetAlreadyObserved) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (!(target && target.nodeType == 1)) { | ||||
|       throw new Error('target must be an Element'); | ||||
|     } | ||||
| 
 | ||||
|     this._registerInstance(); | ||||
|     this._observationTargets.push({ element: target, entry: null }); | ||||
|     this._monitorIntersections(); | ||||
|     this._checkForIntersections(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Stops observing a target element for intersection changes. | ||||
|    * @param {Element} target The DOM element to observe. | ||||
|    */ | ||||
|   unobserve(target) { | ||||
|     this._observationTargets = | ||||
|       this._observationTargets.filter((item) => { | ||||
|         return item.element !== target; | ||||
|       }); | ||||
|     if (!this._observationTargets.length) { | ||||
|       this._unmonitorIntersections(); | ||||
|       this._unregisterInstance(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Stops observing all target elements for intersection changes. | ||||
|    */ | ||||
|   disconnect() { | ||||
|     this._observationTargets = []; | ||||
|     this._unmonitorIntersections(); | ||||
|     this._unregisterInstance(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns any queue entries that have not yet been reported to the | ||||
|    * callback and clears the queue. This can be used in conjunction with the | ||||
|    * callback to obtain the absolute most up-to-date intersection information. | ||||
|    * @return {Array} The currently queued entries. | ||||
|    */ | ||||
|   takeRecords() { | ||||
|     const records = this._queuedEntries.slice(); | ||||
|     this._queuedEntries = []; | ||||
|     return records; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Accepts the threshold value from the user configuration object and | ||||
|    * returns a sorted array of unique threshold values. If a value is not | ||||
|    * between 0 and 1 and error is thrown. | ||||
|    * @private | ||||
|    * @param {Array|number=} optThreshold An optional threshold value or | ||||
|    *     a list of threshold values, defaulting to [0]. | ||||
|    * @return {Array} A sorted list of unique and valid threshold values. | ||||
|    */ | ||||
|   _initThresholds(optThreshold) { | ||||
|     let threshold = optThreshold || [0]; | ||||
|     if (!Array.isArray(threshold)) threshold = [threshold]; | ||||
| 
 | ||||
|     return threshold.sort().filter((t, i, a) => { | ||||
|       if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) { | ||||
|         throw new Error('threshold must be a number between 0 and 1 inclusively'); | ||||
|       } | ||||
|       return t !== a[i - 1]; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Accepts the rootMargin value from the user configuration object | ||||
|    * and returns an array of the four margin values as an object containing | ||||
|    * the value and unit properties. If any of the values are not properly | ||||
|    * formatted or use a unit other than px or %, and error is thrown. | ||||
|    * @private | ||||
|    * @param {string=} optRootMargin An optional rootMargin value, | ||||
|    *     defaulting to '0px'. | ||||
|    * @return {Array<Object>} An array of margin objects with the keys | ||||
|    *     value and unit. | ||||
|    */ | ||||
|   _parseRootMargin(optRootMargin) { | ||||
|     let marginString = optRootMargin || '0px'; | ||||
|     let margins = marginString.split(/\s+/).map((margin) => { | ||||
|       let parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin); | ||||
|       if (!parts) { | ||||
|         throw new Error('rootMargin must be specified in pixels or percent'); | ||||
|       } | ||||
|       return { value: parseFloat(parts[1]), unit: parts[2] }; | ||||
|     }); | ||||
| 
 | ||||
|     // Handles shorthand.
 | ||||
|     margins[1] = margins[1] || margins[0]; | ||||
|     margins[2] = margins[2] || margins[0]; | ||||
|     margins[3] = margins[3] || margins[1]; | ||||
| 
 | ||||
|     return margins; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Starts polling for intersection changes if the polling is not already | ||||
|    * happening, and if the page's visibility state is visible. | ||||
|    * @private | ||||
|    */ | ||||
|   _monitorIntersections() { | ||||
|     if (!this._monitoringIntersections) { | ||||
|       this._monitoringIntersections = true; | ||||
| 
 | ||||
|       // If a poll interval is set, use polling instead of listening to
 | ||||
|       // resize and scroll events or DOM mutations.
 | ||||
|       if (this.POLL_INTERVAL) { | ||||
|         this._monitoringInterval = setInterval(this._checkForIntersections, this.POLL_INTERVAL); | ||||
|       } else { | ||||
|         addEvent(window, 'resize', this._checkForIntersections, true); | ||||
|         addEvent(document, 'scroll', this._checkForIntersections, true); | ||||
| 
 | ||||
|         if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) { | ||||
|           this._domObserver = new MutationObserver(this._checkForIntersections); | ||||
|           this._domObserver.observe(document, { | ||||
|             attributes: true, | ||||
|             childList: true, | ||||
|             characterData: true, | ||||
|             subtree: true, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Stops polling for intersection changes. | ||||
|    * @private | ||||
|    */ | ||||
|   _unmonitorIntersections() { | ||||
|     if (this._monitoringIntersections) { | ||||
|       this._monitoringIntersections = false; | ||||
| 
 | ||||
|       clearInterval(this._monitoringInterval); | ||||
|       this._monitoringInterval = null; | ||||
| 
 | ||||
|       removeEvent(window, 'resize', this._checkForIntersections, true); | ||||
|       removeEvent(document, 'scroll', this._checkForIntersections, true); | ||||
| 
 | ||||
|       if (this._domObserver) { | ||||
|         this._domObserver.disconnect(); | ||||
|         this._domObserver = null; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Scans each observation target for intersection changes and adds them | ||||
|    * to the internal entries queue. If new entries are found, it | ||||
|    * schedules the callback to be invoked. | ||||
|    * @NOTE Using arrow function to bind to `this` instance. | ||||
|    * @private | ||||
|    */ | ||||
|   _checkForIntersections = () => { | ||||
|     let rootIsInDom = this._rootIsInDom(); | ||||
|     let rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect(); | ||||
| 
 | ||||
|     this._observationTargets.forEach(function (item) { | ||||
|       let target = item.element; | ||||
|       let targetRect = getBoundingClientRect(target); | ||||
|       let rootContainsTarget = this._rootContainsTarget(target); | ||||
|       let oldEntry = item.entry; | ||||
|       let intersectionRect = rootIsInDom && rootContainsTarget && | ||||
|         this._computeTargetAndRootIntersection(target, rootRect); | ||||
| 
 | ||||
|       let newEntry = item.entry = new IntersectionObserverEntry({ | ||||
|         time: now(), | ||||
|         target: target, | ||||
|         boundingClientRect: targetRect, | ||||
|         rootBounds: rootRect, | ||||
|         intersectionRect: intersectionRect, | ||||
|       }); | ||||
| 
 | ||||
|       if (!oldEntry) { | ||||
|         this._queuedEntries.push(newEntry); | ||||
|       } else if (rootIsInDom && rootContainsTarget) { | ||||
|         // If the new entry intersection ratio has crossed any of the
 | ||||
|         // thresholds, add a new entry.
 | ||||
|         if (this._hasCrossedThreshold(oldEntry, newEntry)) { | ||||
|           this._queuedEntries.push(newEntry); | ||||
|         } | ||||
|       } else { | ||||
|         // If the root is not in the DOM or target is not contained within
 | ||||
|         // root but the previous entry for this target had an intersection,
 | ||||
|         // add a new record indicating removal.
 | ||||
|         if (oldEntry && oldEntry.isIntersecting) { | ||||
|           this._queuedEntries.push(newEntry); | ||||
|         } | ||||
|       } | ||||
|     }, this); | ||||
| 
 | ||||
|     if (this._queuedEntries.length) { | ||||
|       this._callback(this.takeRecords(), this); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Accepts a target and root rect computes the intersection between then | ||||
|    * following the algorithm in the spec. | ||||
|    * TODO(philipwalton): at this time clip-path is not considered. | ||||
|    * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
 | ||||
|    * @param {Element} target The target DOM element | ||||
|    * @param {Object} rootRect The bounding rect of the root after being | ||||
|    *     expanded by the rootMargin value. | ||||
|    * @return {?Object} The final intersection rect object or undefined if no | ||||
|    *     intersection is found. | ||||
|    * @private | ||||
|    */ | ||||
|   _computeTargetAndRootIntersection(target, rootRect) { | ||||
|     // If the element isn't displayed, an intersection can't happen.
 | ||||
|     if (window.getComputedStyle(target).display == 'none') return; | ||||
| 
 | ||||
|     let targetRect = getBoundingClientRect(target); | ||||
|     let intersectionRect = targetRect; | ||||
|     let parent = getParentNode(target); | ||||
|     let atRoot = false; | ||||
| 
 | ||||
|     while (!atRoot) { | ||||
|       let parentRect = null; | ||||
|       let parentComputedStyle = parent.nodeType == 1 | ||||
|         ? window.getComputedStyle(parent) : {}; | ||||
| 
 | ||||
|       // If the parent isn't displayed, an intersection can't happen.
 | ||||
|       if (parentComputedStyle.display === 'none') return; | ||||
| 
 | ||||
|       if (parent === this.root || parent === document) { | ||||
|         atRoot = true; | ||||
|         parentRect = rootRect; | ||||
|       } else { | ||||
|         // If the element has a non-visible overflow, and it's not the <body>
 | ||||
|         // or <html> element, update the intersection rect.
 | ||||
|         // Note: <body> and <html> cannot be clipped to a rect that's not also
 | ||||
|         // the document rect, so no need to compute a new intersection.
 | ||||
|         if (parent !== document.body && | ||||
|           parent !== document.documentElement && | ||||
|           parentComputedStyle.overflow !== 'visible') { | ||||
|           parentRect = getBoundingClientRect(parent); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // If either of the above conditionals set a new parentRect,
 | ||||
|       // calculate new intersection data.
 | ||||
|       if (parentRect) { | ||||
|         intersectionRect = computeRectIntersection(parentRect, intersectionRect); | ||||
| 
 | ||||
|         if (!intersectionRect) break; | ||||
|       } | ||||
|       parent = getParentNode(parent); | ||||
|     } | ||||
|     return intersectionRect; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the root rect after being expanded by the rootMargin value. | ||||
|    * @return {Object} The expanded root rect. | ||||
|    * @private | ||||
|    */ | ||||
|   _getRootRect() { | ||||
|     let rootRect; | ||||
|     if (this.root) { | ||||
|       rootRect = getBoundingClientRect(this.root); | ||||
|     } else { | ||||
|       // Use <html>/<body> instead of window since scroll bars affect size.
 | ||||
|       let html = document.documentElement; | ||||
|       let { body } = document; | ||||
|       rootRect = { | ||||
|         top: 0, | ||||
|         left: 0, | ||||
|         right: html.clientWidth || body.clientWidth, | ||||
|         width: html.clientWidth || body.clientWidth, | ||||
|         bottom: html.clientHeight || body.clientHeight, | ||||
|         height: html.clientHeight || body.clientHeight, | ||||
|       }; | ||||
|     } | ||||
|     return this._expandRectByRootMargin(rootRect); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Accepts a rect and expands it by the rootMargin value. | ||||
|    * @param {Object} rect The rect object to expand. | ||||
|    * @return {Object} The expanded rect. | ||||
|    * @private | ||||
|    */ | ||||
|   _expandRectByRootMargin(rect) { | ||||
|     let margins = this._rootMarginValues.map((margin, i) => { | ||||
|       return margin.unit === 'px' ? margin.value | ||||
|         : margin.value * (i % 2 ? rect.width : rect.height) / 100; | ||||
|     }); | ||||
|     let newRect = { | ||||
|       top: rect.top - margins[0], | ||||
|       right: rect.right + margins[1], | ||||
|       bottom: rect.bottom + margins[2], | ||||
|       left: rect.left - margins[3], | ||||
|     }; | ||||
|     newRect.width = newRect.right - newRect.left; | ||||
|     newRect.height = newRect.bottom - newRect.top; | ||||
| 
 | ||||
|     return newRect; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Accepts an old and new entry and returns true if at least one of the | ||||
|    * threshold values has been crossed. | ||||
|    * @param {?IntersectionObserverEntry} oldEntry The previous entry for a | ||||
|    *    particular target element or null if no previous entry exists. | ||||
|    * @param {IntersectionObserverEntry} newEntry The current entry for a | ||||
|    *    particular target element. | ||||
|    * @return {boolean} Returns true if a any threshold has been crossed. | ||||
|    * @private | ||||
|    */ | ||||
|   _hasCrossedThreshold(oldEntry, newEntry) { | ||||
|     // To make comparing easier, an entry that has a ratio of 0
 | ||||
|     // but does not actually intersect is given a value of -1
 | ||||
|     const oldRatio = oldEntry && oldEntry.isIntersecting | ||||
|       ? oldEntry.intersectionRatio || 0 : -1; | ||||
|     const newRatio = newEntry.isIntersecting | ||||
|       ? newEntry.intersectionRatio || 0 : -1; | ||||
| 
 | ||||
|     // Ignore unchanged ratios
 | ||||
|     if (oldRatio === newRatio) return; | ||||
| 
 | ||||
|     for (let i = 0; i < this.thresholds.length; i++) { | ||||
|       const threshold = this.thresholds[i]; | ||||
| 
 | ||||
|       // Return true if an entry matches a threshold or if the new ratio
 | ||||
|       // and the old ratio are on the opposite sides of a threshold.
 | ||||
|       if (threshold == oldRatio || threshold == newRatio || | ||||
|         threshold < oldRatio !== threshold < newRatio) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns whether or not the root element is an element and is in the DOM. | ||||
|    * @return {boolean} True if the root element is an element and is in the DOM. | ||||
|    * @private | ||||
|    */ | ||||
|   _rootIsInDom() { | ||||
|     return !this.root || containsDeep(document, this.root); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns whether or not the target element is a child of root. | ||||
|    * @param {Element} target The target element to check. | ||||
|    * @return {boolean} True if the target element is a child of root. | ||||
|    * @private | ||||
|    */ | ||||
|   _rootContainsTarget(target) { | ||||
|     return containsDeep(this.root || document, target); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Adds the instance to the global IntersectionObserver registry if it isn't | ||||
|    * already present. | ||||
|    * @private | ||||
|    */ | ||||
|   _registerInstance() { | ||||
|     if (registry.indexOf(this) < 0) { | ||||
|       registry.push(this); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Removes the instance from the global IntersectionObserver registry. | ||||
|    * @private | ||||
|    */ | ||||
|   _unregisterInstance() { | ||||
|     const index = registry.indexOf(this); | ||||
|     if (index !== -1) registry.splice(index, 1); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns the result of the performance.now() method or null in browsers | ||||
|  * that don't support the API. | ||||
|  * @return {number} The elapsed time since the page was requested. | ||||
|  */ | ||||
| function now() { | ||||
|   return window.performance && performance.now && performance.now(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Throttles a function and delays its execution, so it's only called at most | ||||
|  * once within a given time period. | ||||
|  * @param {Function} fn The function to throttle. | ||||
|  * @param {number} timeout The amount of time that must pass before the | ||||
|  *     function can be called again. | ||||
|  * @return {Function} The throttled function. | ||||
|  */ | ||||
| function throttle(fn, timeout) { | ||||
|   let timer = null; | ||||
|   return function () { | ||||
|     if (!timer) { | ||||
|       timer = setTimeout(() => { | ||||
|         fn(); | ||||
|         timer = null; | ||||
|       }, timeout); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Adds an event handler to a DOM node ensuring cross-browser compatibility. | ||||
|  * @param {Node} node The DOM node to add the event handler to. | ||||
|  * @param {string} event The event name. | ||||
|  * @param {Function} fn The event handler to add. | ||||
|  * @param {boolean} opt_useCapture Optionally adds the even to the capture | ||||
|  *     phase. Note: this only works in modern browsers. | ||||
|  */ | ||||
| function addEvent(node, event, fn, opt_useCapture) { | ||||
|   if (typeof node.addEventListener == 'function') { | ||||
|     node.addEventListener(event, fn, opt_useCapture || false); | ||||
|   } else if (typeof node.attachEvent == 'function') { | ||||
|     node.attachEvent(`on${event}`, fn); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Removes a previously added event handler from a DOM node. | ||||
|  * @param {Node} node The DOM node to remove the event handler from. | ||||
|  * @param {string} event The event name. | ||||
|  * @param {Function} fn The event handler to remove. | ||||
|  * @param {boolean} opt_useCapture If the event handler was added with this | ||||
|  *     flag set to true, it should be set to true here in order to remove it. | ||||
|  */ | ||||
| function removeEvent(node, event, fn, opt_useCapture) { | ||||
|   if (typeof node.removeEventListener == 'function') { | ||||
|     node.removeEventListener(event, fn, opt_useCapture || false); | ||||
|   } else if (typeof node.detatchEvent == 'function') { | ||||
|     node.detatchEvent(`on${event}`, fn); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Returns the intersection between two rect objects. | ||||
|  * @param {Object} rect1 The first rect. | ||||
|  * @param {Object} rect2 The second rect. | ||||
|  * @return {?Object} The intersection rect or undefined if no intersection | ||||
|  *     is found. | ||||
|  */ | ||||
| function computeRectIntersection(rect1, rect2) { | ||||
|   const top = Math.max(rect1.top, rect2.top); | ||||
|   const bottom = Math.min(rect1.bottom, rect2.bottom); | ||||
|   const left = Math.max(rect1.left, rect2.left); | ||||
|   const right = Math.min(rect1.right, rect2.right); | ||||
|   const width = right - left; | ||||
|   const height = bottom - top; | ||||
| 
 | ||||
|   return width >= 0 && height >= 0 && { top, bottom, left, right, width, height }; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Shims the native getBoundingClientRect for compatibility with older IE. | ||||
|  * @param {Element} el The element whose bounding rect to get. | ||||
|  * @return {Object} The (possibly shimmed) rect of the element. | ||||
|  */ | ||||
| function getBoundingClientRect(el) { | ||||
|   let rect; | ||||
| 
 | ||||
|   try { | ||||
|     rect = el.getBoundingClientRect(); | ||||
|   } catch (err) { | ||||
|     // Ignore Windows 7 IE11 "Unspecified error"
 | ||||
|     // https://github.com/w3c/IntersectionObserver/pull/205
 | ||||
|   } | ||||
| 
 | ||||
|   if (!rect) return getEmptyRect(); | ||||
| 
 | ||||
|   // Older IE
 | ||||
|   if (!(rect.width && rect.height)) { | ||||
|     rect = { | ||||
|       top: rect.top, | ||||
|       right: rect.right, | ||||
|       bottom: rect.bottom, | ||||
|       left: rect.left, | ||||
|       width: rect.right - rect.left, | ||||
|       height: rect.bottom - rect.top, | ||||
|     }; | ||||
|   } | ||||
|   return rect; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Returns an empty rect object. An empty rect is returned when an element | ||||
|  * is not in the DOM. | ||||
|  * @return {Object} The empty rect. | ||||
|  */ | ||||
| function getEmptyRect() { | ||||
|   return { | ||||
|     top: 0, | ||||
|     bottom: 0, | ||||
|     left: 0, | ||||
|     right: 0, | ||||
|     width: 0, | ||||
|     height: 0, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Checks to see if a parent element contains a child element (including inside | ||||
|  * shadow DOM). | ||||
|  * @param {Node} parent The parent element. | ||||
|  * @param {Node} child The child element. | ||||
|  * @return {boolean} True if the parent node contains the child node. | ||||
|  */ | ||||
| function containsDeep(parent, child) { | ||||
|   let node = child; | ||||
|   while (node) { | ||||
|     if (node === parent) return true; | ||||
| 
 | ||||
|     node = getParentNode(node); | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Gets the parent node of an element or its host element if the parent node | ||||
|  * is a shadow root. | ||||
|  * @param {Node} node The node whose parent to get. | ||||
|  * @return {Node|null} The parent node or null if no parent exists. | ||||
|  */ | ||||
| function getParentNode(node) { | ||||
|   let parent = node.parentNode; | ||||
| 
 | ||||
|   if (parent && parent.nodeType == 11 && parent.host) { | ||||
|     // If the parent is a shadow root, return the host element.
 | ||||
|     return parent.host; | ||||
|   } | ||||
| 
 | ||||
|   if (parent && parent.assignedSlot) { | ||||
|     // If the parent is distributed in a <slot>, return the parent of a slot.
 | ||||
|     return parent.assignedSlot.parentNode; | ||||
|   } | ||||
| 
 | ||||
|   return parent; | ||||
| } | ||||
|  | @ -0,0 +1,124 @@ | |||
| // Handle appear and disappear event.
 | ||||
| // Fork from https://github.com/raxjs/appear-polyfill
 | ||||
| // @ts-ignore
 | ||||
| import PolyfilledIntersectionObserver from './intersection-observer'; | ||||
| 
 | ||||
| enum VisibilityChangeEvent { | ||||
|   appear = 'appear', | ||||
|   disappear = 'disappear', | ||||
| } | ||||
| 
 | ||||
| enum VisibilityChangeDirection { | ||||
|   up = 'up', | ||||
|   down = 'down', | ||||
| } | ||||
| 
 | ||||
| // Shared intersectionObserver instance.
 | ||||
| let intersectionObserver: any; | ||||
| const IntersectionObserver = (function () { | ||||
|   if (typeof window !== 'undefined' && | ||||
|     'IntersectionObserver' in window && | ||||
|     'IntersectionObserverEntry' in window && | ||||
|     'intersectionRatio' in window.IntersectionObserverEntry.prototype) { | ||||
|     // features are natively supported
 | ||||
|     return window.IntersectionObserver; | ||||
|   } else { | ||||
|     // polyfilled IntersectionObserver
 | ||||
|     return PolyfilledIntersectionObserver; | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| function generateThreshold(number: number) { | ||||
|   const thresholds = []; | ||||
|   for (let index = 0; index < number; index++) { | ||||
|     thresholds.push(index / number); | ||||
|   } | ||||
| 
 | ||||
|   return thresholds; | ||||
| } | ||||
| 
 | ||||
| const defaultOptions = { | ||||
|   // @ts-ignore
 | ||||
|   root: null, | ||||
|   rootMargin: '0px', | ||||
|   threshold: generateThreshold(10), | ||||
| }; | ||||
| 
 | ||||
| export function createIntersectionObserver(options = defaultOptions) { | ||||
|   intersectionObserver = new IntersectionObserver(handleIntersect, options); | ||||
| } | ||||
| 
 | ||||
| export function destroyIntersectionObserver() { | ||||
|   if (intersectionObserver) { | ||||
|     intersectionObserver.disconnect(); | ||||
|     intersectionObserver = null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function observerElement(element: HTMLElement | Node) { | ||||
|   if (!intersectionObserver) createIntersectionObserver(); | ||||
| 
 | ||||
|   if (element === document) element = document.documentElement; | ||||
| 
 | ||||
|   intersectionObserver.observe(element); | ||||
| } | ||||
| 
 | ||||
| function handleIntersect(entries: IntersectionObserverEntry[]) { | ||||
|   entries.forEach((entry) => { | ||||
|     const { | ||||
|       target, | ||||
|       boundingClientRect, | ||||
|       intersectionRatio, | ||||
|     } = entry; | ||||
|     // No `top` value in polyfill.
 | ||||
|     const currentY = boundingClientRect.y || boundingClientRect.top; | ||||
|     const beforeY = parseInt(target.getAttribute('data-before-current-y')) || currentY; | ||||
| 
 | ||||
|     // is in view
 | ||||
|     if ( | ||||
|       intersectionRatio > 0.01 && | ||||
|       !isTrue(target.getAttribute('data-appeared')) && | ||||
|       !appearOnce(target as HTMLElement, VisibilityChangeEvent.appear) | ||||
|     ) { | ||||
|       target.setAttribute('data-appeared', 'true'); | ||||
|       target.setAttribute('data-has-appeared', 'true'); | ||||
|       target.dispatchEvent(createEvent(VisibilityChangeEvent.appear, { | ||||
|         direction: currentY > beforeY ? VisibilityChangeDirection.up : VisibilityChangeDirection.down, | ||||
|       })); | ||||
|     } else if ( | ||||
|       intersectionRatio === 0 && | ||||
|       isTrue(target.getAttribute('data-appeared')) && | ||||
|       !appearOnce(target as HTMLElement, VisibilityChangeEvent.disappear) | ||||
|     ) { | ||||
|       target.setAttribute('data-appeared', 'false'); | ||||
|       target.setAttribute('data-has-disappeared', 'true'); | ||||
|       target.dispatchEvent(createEvent(VisibilityChangeEvent.appear, { | ||||
|         direction: currentY > beforeY ? VisibilityChangeDirection.up : VisibilityChangeDirection.down, | ||||
|       })); | ||||
|     } | ||||
| 
 | ||||
|     target.setAttribute('data-before-current-y', String(currentY)); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * need appear again when node has isonce or data-once | ||||
|  */ | ||||
| function appearOnce(node: HTMLElement, type: VisibilityChangeEvent) { | ||||
|   const isOnce = isTrue(node.getAttribute('isonce')) || isTrue(node.getAttribute('data-once')); | ||||
|   const appearType = type === VisibilityChangeEvent.appear ? 'data-has-appeared' : 'data-has-disappeared'; | ||||
| 
 | ||||
|   return isOnce && isTrue(node.getAttribute(appearType)); | ||||
| } | ||||
| 
 | ||||
| function isTrue(flag: any) { | ||||
|   return flag && flag !== 'false'; | ||||
| } | ||||
| 
 | ||||
| function createEvent(eventName: string, data: any) { | ||||
|   return new CustomEvent(eventName, { | ||||
|     bubbles: false, | ||||
|     cancelable: true, | ||||
|     detail: data, | ||||
|   }); | ||||
| } | ||||
|  | @ -40,7 +40,7 @@ describe('createElement', () => { | |||
|     render(createElement( | ||||
|       'div', | ||||
|       { | ||||
|         onDisappear: func | ||||
|         onDisappear: func, | ||||
|       }, | ||||
|       str | ||||
|     )); | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ | |||
|   "bugs": "https://github.com/ice-lab/ice-next/issues", | ||||
|   "homepage": "https://next.ice.work", | ||||
|   "devDependencies": { | ||||
|     "@builder/swc": "^0.2.0", | ||||
|     "@ice/route-manifest": "^1.0.0", | ||||
|     "@ice/runtime": "^1.0.0", | ||||
|     "build-scripts": "^2.0.0-23", | ||||
|  | @ -39,9 +40,9 @@ | |||
|     "fork-ts-checker-webpack-plugin": "7.2.6", | ||||
|     "react": "^18.0.0", | ||||
|     "terser": "^5.12.1", | ||||
|     "typescript": "^4.6.4", | ||||
|     "unplugin": "^0.3.2", | ||||
|     "webpack": "^5.73.0", | ||||
|     "webpack-dev-server": "^4.7.4", | ||||
|     "typescript": "^4.6.4" | ||||
|     "webpack-dev-server": "^4.7.4" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import type { ForkTsCheckerWebpackPluginOptions } from 'fork-ts-checker-webpack- | |||
| import type { UnpluginOptions } from 'unplugin'; | ||||
| import type Server from 'webpack-dev-server'; | ||||
| import type { ECMA } from 'terser'; | ||||
| import type { Config as CompilationConfig } from '@builder/swc'; | ||||
| 
 | ||||
| // get type definitions from terser-webpack-plugin
 | ||||
| interface CustomOptions { | ||||
|  | @ -27,6 +28,7 @@ interface ConfigurationCtx extends Config { | |||
| interface SwcOptions { | ||||
|   jsxTransform?: boolean; | ||||
|   removeExportExprs?: string[]; | ||||
|   compilationConfig?: CompilationConfig; | ||||
| } | ||||
| 
 | ||||
| type Experimental = Pick<Configuration, 'experiments'>; | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ const compilationPlugin = (options: Options): UnpluginOptions => { | |||
|         filename: id, | ||||
|       }; | ||||
| 
 | ||||
|       const { jsxTransform, removeExportExprs } = swcOptions; | ||||
|       const { jsxTransform, removeExportExprs, compilationConfig } = swcOptions; | ||||
| 
 | ||||
|       let needTransform = false; | ||||
| 
 | ||||
|  | @ -81,7 +81,7 @@ const compilationPlugin = (options: Options): UnpluginOptions => { | |||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         const output = await transform(source, programmaticOptions); | ||||
|         const output = await transform(source, merge(programmaticOptions, compilationConfig || {})); | ||||
|         const { code } = output; | ||||
|         let { map } = output; | ||||
|         // FIXME: swc transform should return the sourcemap which the type is object
 | ||||
|  |  | |||
							
								
								
									
										1549
									
								
								pnpm-lock.yaml
								
								
								
								
							
							
						
						
									
										1549
									
								
								pnpm-lock.yaml
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,41 @@ | |||
| import { expect, test, describe, afterAll } from 'vitest'; | ||||
| import { buildFixture, setupBrowser } from '../utils/build'; | ||||
| import { startFixture, setupStartBrowser } from '../utils/start'; | ||||
| import { Page } from '../utils/browser'; | ||||
| 
 | ||||
| const example = 'rax-project'; | ||||
| 
 | ||||
| describe(`build ${example}`, () => { | ||||
|   let page: Page = null; | ||||
|   let browser = null; | ||||
| 
 | ||||
|   test('open /', async () => { | ||||
|     await buildFixture(example); | ||||
|     const res = await setupBrowser({ example }); | ||||
| 
 | ||||
|     page = res.page; | ||||
|     browser = res.browser; | ||||
|     expect(await page.$$text('div')).toStrictEqual(['']); | ||||
|   }, 120000); | ||||
| 
 | ||||
|   afterAll(async () => { | ||||
|     await browser.close(); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe(`start ${example}`, () => { | ||||
|   let page: Page = null; | ||||
|   let browser = null; | ||||
| 
 | ||||
|   test('setup devServer', async () => { | ||||
|     const { devServer, port } = await startFixture(example); | ||||
|     const res = await setupStartBrowser({ server: devServer, port }); | ||||
|     page = res.page; | ||||
|     browser = res.browser; | ||||
|     expect((await page.$$text('span'))[0]).toStrictEqual('Welcome to Your Rax App'); | ||||
|   }, 120000); | ||||
| 
 | ||||
|   afterAll(async () => { | ||||
|     await browser.close(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -12,7 +12,8 @@ interface ISetupBrowser { | |||
|     example: string; | ||||
|     outputDir?: string; | ||||
|     defaultHtml?: string; | ||||
|   }): Promise<IReturn>; | ||||
|     disableJS?: boolean; | ||||
|   }): Promise<ReturnValue>; | ||||
| } | ||||
| 
 | ||||
| interface IReturn { | ||||
|  | @ -29,12 +30,12 @@ export const buildFixture = async function(example: string) { | |||
| } | ||||
| 
 | ||||
| export const setupBrowser: SetupBrowser = async (options) => { | ||||
|   const { example, outputDir = 'build', defaultHtml = 'index.html' } = options; | ||||
|   const { example, outputDir = 'build', defaultHtml = 'index.html', disableJS = true } = options; | ||||
|   const rootDir = path.join(__dirname, `../../examples/${example}`); | ||||
|   const port = await getPort(); | ||||
|   const browser = new Browser({ cwd: path.join(rootDir, outputDir), port }); | ||||
|   await browser.start(); | ||||
|   const disableJS = true; | ||||
|   await browser.start('disableJS', disableJS); | ||||
|   console.log() | ||||
|   // when preview html generate by build, the path will not match the router info, so hydrate will not found the route component
 | ||||
|   const page = await browser.page(`http://127.0.0.1:${port}/${defaultHtml}`, disableJS); | ||||
|   return { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue