8 |
9 |
31 |
33 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@exposure-lib/core",
3 | "version": "2.0.3",
4 | "description": "@exposure-lib/core",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.esm.js",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.cjs.js",
11 | "import": "./dist/index.esm.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/hubvue/exposure-lib.git",
18 | "directory": "packages/core"
19 | },
20 | "keywords": [
21 | "InterfaceObserver",
22 | "exposure"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/hubvue/exposure-lib/issues"
26 | },
27 | "homepage": "https://github.com/hubvue/exposure-lib/tree/main/packages/core#readme",
28 | "author": "hubvue",
29 | "license": "MIT"
30 | }
31 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | // testEnvironment: 'jest-environment-node',
3 | // testEnvironment: 'jsdom',
4 | preset: 'jest-puppeteer',
5 | transform: {
6 | '^.+\\.ts?$': 'ts-jest',
7 | },
8 | clearMocks: true,
9 | collectCoverage: true,
10 | coverageDirectory: 'coverage',
11 | coveragePathIgnorePatterns: ['/node_modules/'],
12 | coverageProvider: 'babel',
13 | coverageReporters: ['html', 'lcov', 'text'],
14 | errorOnDeprecated: false,
15 | maxWorkers: '50%',
16 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
17 | rootDir: __dirname,
18 | testMatch: ['/tests/**/*spec.[jt]s?(x)'],
19 | testPathIgnorePatterns: ['/node_modules/'],
20 | watchPathIgnorePatterns: [
21 | '/node_modules/',
22 | '/dist/',
23 | '/.git/',
24 | '/docs/',
25 | './vscode',
26 | '/.github/',
27 | '/scripts/',
28 | '/temp/',
29 | ],
30 | }
31 |
--------------------------------------------------------------------------------
/packages/polyfill/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@exposure-lib/polyfill",
3 | "version": "2.0.3",
4 | "description": "@exposure-lib/polyfill",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.esm.js",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.cjs.js",
11 | "import": "./dist/index.esm.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/hubvue/exposure-lib.git",
18 | "directory": "packages/polyfill"
19 | },
20 | "keywords": [
21 | "InterfaceObserver",
22 | "exposure"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/hubvue/exposure-lib/issues"
26 | },
27 | "homepage": "https://github.com/hubvue/exposure-lib/tree/main/packages/polyfill#readme",
28 | "author": "hubvue",
29 | "license": "MIT"
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 | - name: Install pnpm
19 | uses: pnpm/action-setup@v2.2.2
20 | with:
21 | version: 6.15.1
22 |
23 | - name: Use Node.js v16
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: v16
27 | registry-url: https://registry.npmjs.org/
28 | cache: "pnpm"
29 |
30 | - name: Install Dependencies
31 | run: pnpm install
32 |
33 | - name: Test
34 | run: pnpm run test
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/vue2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@exposure-lib/vue2",
3 | "version": "2.0.3",
4 | "description": "@exposure-lib/vue2",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.esm.js",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.cjs.js",
11 | "import": "./dist/index.esm.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/hubvue/exposure-lib.git",
18 | "directory": "packages/vue2"
19 | },
20 | "keywords": [
21 | "InterfaceObserver",
22 | "exposure"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/hubvue/exposure-lib/issues"
26 | },
27 | "homepage": "https://github.com/hubvue/exposure-lib/tree/main/packages/vue2#readme",
28 | "author": "hubvue",
29 | "license": "MIT",
30 | "peerDependencies": {
31 | "vue": ">=2.6.11",
32 | "@exposure-lib/core": ">=2.0.0"
33 | },
34 | "devDependencies": {
35 | "vue": "^2.6.11"
36 | },
37 | "dependencies": {
38 | "@exposure-lib/core": "^2.0.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - name: Install pnpm
16 | uses: pnpm/action-setup@v2.2.2
17 | with:
18 | version: 6.15.1
19 |
20 | - name: Use Node.js v16
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: v16
24 | registry-url: https://registry.npmjs.org/
25 | cache: "pnpm"
26 |
27 | - run: npx conventional-github-releaser -p angular
28 | continue-on-error: true
29 | env:
30 | CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}}
31 |
32 | - name: Install Dependencies
33 | run: pnpm install
34 |
35 | - name: Build
36 | run: pnpm run build:all
37 |
38 | - name: Publish
39 | run: pnpm -r publish --access public --no-git-checks
40 | env:
41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
42 |
--------------------------------------------------------------------------------
/packages/core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/vue/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/vue2/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/polyfill/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@exposure-lib/vue",
3 | "version": "2.0.3",
4 | "description": "@exposure-lib/vue",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.esm.js",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.cjs.js",
11 | "import": "./dist/index.esm.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/hubvue/exposure-lib.git",
18 | "directory": "packages/vue"
19 | },
20 | "keywords": [
21 | "InterfaceObserver",
22 | "exposure",
23 | "vue"
24 | ],
25 | "bugs": {
26 | "url": "https://github.com/hubvue/exposure-lib/issues"
27 | },
28 | "homepage": "https://github.com/hubvue/exposure-lib/tree/main/packages/vue#readme",
29 | "author": "hubvue",
30 | "license": "MIT",
31 | "peerDependencies": {
32 | "vue": ">=3.2.31",
33 | "@exposure-lib/core": ">=2.0.0"
34 | },
35 | "devDependencies": {
36 | "vue": "^3.2.31"
37 | },
38 | "dependencies": {
39 | "@exposure-lib/core": "^2.0.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/example/src/router/routes.ts:
--------------------------------------------------------------------------------
1 | import CoreBase from '../pages/core/index.vue'
2 | import CoreThreshold from '../pages/core/threshold.vue'
3 | import CoreReset from '../pages/core/reset.vue'
4 | import CoreUnobserve from '../pages/core/unobserve.vue'
5 |
6 | import VueBase from '../pages/vue/index.vue'
7 | import VueThreshold from '../pages/vue/threshold.vue'
8 | import VueReset from '../pages/vue/reset.vue'
9 |
10 | export default [
11 | {
12 | name: "CoreBase",
13 | path: '/core-base',
14 | component: CoreBase
15 | },
16 | {
17 | name: "CoreThreshold",
18 | path: '/core-threshold',
19 | component: CoreThreshold
20 | },
21 | {
22 | name: "CoreReset",
23 | path: '/core-reset',
24 | component: CoreReset
25 | },
26 | {
27 | name: "CoreUnobserve",
28 | path: '/core-unobserve',
29 | component: CoreUnobserve
30 | },
31 | {
32 | name: "VueBase",
33 | path: '/vue-base',
34 | component: VueBase
35 | },
36 | {
37 | name: "VueThreshold",
38 | path: '/vue-threshold',
39 | component: VueThreshold
40 | },
41 | {
42 | name: 'VueReset',
43 | path: '/vue-reset',
44 | component: VueReset
45 | }
46 | ]
47 |
--------------------------------------------------------------------------------
/packages/example/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
{{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VS Code
15 | +
16 | Volar
17 |
124 |
125 |
126 |
136 | ```
137 |
138 | 使用 Vue 动态指令参数的方式对指令传参,所传值必须是`[0,1]`之间的数值,这样在监听曝光的时候就会按照所传值的比例进行曝光。
139 |
140 | > 需要注意:元素级 threshold > 全局级 threshold
141 |
142 | ### \$resetExposure
143 |
144 | 曝光回调的执行是单例的,也就是说当曝光过一次并且回调执行后,再次曝光就不会再执行回调函数。
145 |
146 | 在 Vue 组件中存在 KeepAlive 的场景,当 KeepAlive 组件切换的时候曝光回调也不会重新执行。这种情况下如果想要重新执行就需要使用`$resetExposure`API 去重置元素状态。
147 |
148 | ```js
149 | deactivated() {
150 | this.$resetExposure()
151 | }
152 | ```
153 |
154 | 当调用`this.$resetExposure()`不传入任何参数的时候讲会把当前实例中所有监听元素的执行状态全部重置。如果需要只重置某个元素的执行状态,需要传入当前元素。
155 |
156 | ```js
157 | deactivated() {
158 | this.$resetExposure(this.$refs.el)
159 | }
160 | ```
161 |
162 | #### Vue 2 + composition-api
163 | 若项目使用Vue 2 + composition-api构建,为了遵循composition-api 编码规范,则可以使用useResetExposure 重置曝光。
164 |
165 | ```ts
166 | import { useResetExposure } from 'vue-exposure'
167 | import { defineComponent, onDeactivated } from '@vue/composition-api'
168 | export default defineComponent({
169 | setup() {
170 | onDeactivated(() => {
171 | useResetExposure()
172 | })
173 | }
174 | })
175 | ```
--------------------------------------------------------------------------------
/tests/core.spec.ts:
--------------------------------------------------------------------------------
1 | import { Page } from 'puppeteer'
2 |
3 | // set default timeout
4 | jest.setTimeout(1000000)
5 |
6 | const getCount = async (page: Page) => {
7 | return page.$$eval('.count', (els) => {
8 | return els[0].textContent
9 | })
10 | }
11 |
12 | describe('core#base', () => {
13 | let page = (global as any).page as Page
14 | beforeAll(async () => {
15 | await page.goto('http://0.0.0.0:3000/#/core-base')
16 | })
17 |
18 | test('should be an element trigger at the beginning', async () => {
19 | const count = await getCount(page)
20 | expect(count).toBe('1')
21 | })
22 |
23 | test('when scrolling to middle appears, count should be added by 1', async () => {
24 | await page.evaluate(() => {
25 | window.scrollTo(0, 700)
26 | })
27 | await page.waitForTimeout(1000)
28 | const count = await getCount(page)
29 |
30 | expect(count).toBe('2')
31 | })
32 |
33 | test('when the middle element leaves the visible area, count should be added by 1', async () => {
34 | await page.evaluate(() => {
35 | window.scrollTo(0, 1500)
36 | })
37 | await page.waitForTimeout(1000)
38 | const count = await getCount(page)
39 | expect(count).toBe('3')
40 | })
41 |
42 | test('when the bottom element leaves the visible area, count should be added by 1', async () => {
43 | await page.evaluate(() => {
44 | window.scrollTo(0, 2500)
45 | })
46 | await page.waitForTimeout(1000)
47 | const count = await getCount(page)
48 | expect(count).toBe('4')
49 | })
50 | })
51 |
52 | describe('core#threshold', () => {
53 | let page = (global as any).page as Page
54 | beforeAll(async () => {
55 | await page.goto('http://0.0.0.0:3000/#/core-threshold')
56 | })
57 |
58 | test('when the middle element is exposed to half of the visible area, count should be added by 1', async () => {
59 | await page.evaluate(() => {
60 | window.scrollTo(0, 595)
61 | })
62 |
63 | await page.waitForTimeout(1000)
64 | const count = await getCount(page)
65 | expect(count).toBe('2')
66 | })
67 | })
68 |
69 | describe('core#reset', () => {
70 | let page = (global as any).page as Page
71 | beforeAll(async () => {
72 | await page.goto('http://0.0.0.0:3000/#/core-reset')
73 | })
74 |
75 | test('The node state should be reset after resetExposure is triggered', async () => {
76 | await page.evaluate(() => {
77 | window.scrollTo(0, 0)
78 | })
79 | await page.waitForTimeout(1000)
80 | // 1 滚动到中间触发middle
81 | await page.evaluate(() => {
82 | window.scrollTo(0, 800)
83 | })
84 | await page.waitForTimeout(1000)
85 |
86 | // 滚动到底部触发bottom
87 | await page.evaluate(() => {
88 | window.scrollTo(0, 2500)
89 | })
90 | await page.waitForTimeout(1000)
91 |
92 | // 再次滚动到中间触发middle
93 | await page.evaluate(() => {
94 | window.scrollTo(0, 800)
95 | })
96 | await page.waitForTimeout(1000)
97 |
98 | // 再次滚动到顶部触发top
99 | await page.evaluate(() => {
100 | window.scrollTo(0, 0)
101 | })
102 | await page.waitForTimeout(1000)
103 |
104 | // // 再次滚动到中间触发middle
105 | await page.evaluate(() => {
106 | window.scrollTo(0, 800)
107 | })
108 | await page.waitForTimeout(1000)
109 |
110 | const count = await getCount(page)
111 | expect(count).toBe('6')
112 | })
113 | })
114 |
115 | describe('core#unobserve', () => {
116 | let page = (global as any).page as Page
117 | beforeAll(async () => {
118 | await page.goto('http://0.0.0.0:3000/#/core-unobserve')
119 | })
120 |
121 | test('The node should be unlistened to after the unobserve is triggered', async () => {
122 | await page.evaluate(() => {
123 | window.scrollTo(0, 0)
124 | })
125 | await page.waitForTimeout(1000)
126 | // 1 滚动到中间触发middle
127 | await page.evaluate(() => {
128 | window.scrollTo(0, 800)
129 | })
130 | await page.waitForTimeout(1000)
131 |
132 | // 滚动到底部触发bottom
133 | await page.evaluate(() => {
134 | window.scrollTo(0, 2500)
135 | })
136 | await page.waitForTimeout(1000)
137 |
138 | // 再次滚动到中间触发middle
139 | await page.evaluate(() => {
140 | window.scrollTo(0, 800)
141 | })
142 | await page.waitForTimeout(1000)
143 |
144 | // 再次滚动到顶部不会触发top
145 | await page.evaluate(() => {
146 | window.scrollTo(0, 0)
147 | })
148 | await page.waitForTimeout(1000)
149 |
150 | const count = await getCount(page)
151 | expect(count).toBe('4')
152 | })
153 | })
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # exposure-lib
2 |
3 | [中文文档](./README.zh-CN.md)
4 |
5 | [Support Vue 2.x Doc](./packages/vue2/README.md)
6 |
7 | [Support Vue 3.x Doc](./packages/vue/README.md)
8 |
9 |
10 | Based on the InterfaceObserver API, listens for elements to be visible or not, and executes a callback function when the element appears in the viewport.
11 |
12 | ## Quick Start
13 |
14 | ### Install
15 |
16 | > pnpm add @exposure-lib/core
17 |
18 | Since the compatibility of the InterfaceObserver API is still not well supported on some low version browsers, you can introduce polyfill to `@exposure-lib/core` beforehand to use it normally.
19 |
20 | > pnpm add @exposure-lib/polyfill
21 |
22 | **Introducing the package**
23 |
24 | ```ts
25 | import '@exposure-lib/polyfill'
26 | import * as Exposure from '@exposure-lib/core'
27 | ```
28 |
29 | > Note: the polyfill package must be introduced before the core package
30 |
31 | ### Usage
32 |
33 | Using `exposure` to listen to whether an element appears in the visible area is very simple and requires only two steps.
34 |
35 | 1. First you need to create an `Exposure` to listen to the element, which is created by the `createExposure` method.
36 |
37 | ```ts
38 | import { createExposure } from '@exposure-lib/core'
39 | const exposure = createExposure()
40 | ```
41 |
42 | 2. Then call the `observe` method of `Exposure` to listen for the element
43 |
44 | ```ts
45 | const el = document.getElementById('el')
46 | exposure.observe(el, () => {
47 | console.log('exposure')
48 | })
49 | ```
50 | The `exposure.observe` method accepts at least two arguments, the first one is an element of type Element, the second one is a Handler, which is executed when the monitored element appears in the visible area, and the third one is a listening threshold (optional).
51 |
52 |
53 | ### threshold
54 |
55 | By default, the execution of the exposure callback waits for the entire bound element to be fully wrapped before it is executed. If you have a need to expose an element when a certain percentage of it appears, the
56 | you can set the threshold, using the following two methods.
57 |
58 | #### Exposure threshold
59 |
60 | Each call to the `createExposure` method to create an `Exposure` supports passing in a threshold for use by elements under the current `Exposure` scope.
61 |
62 | ```ts
63 | const exposure = createExposure(0.2)
64 | ```
65 |
66 | As shown in the code above, the callback function is executed when the exposure ratio of the element reaches 0.2.
67 |
68 | #### Element threshold
69 |
70 | If you want the exposure ratio of an element to be different from that of other elements, you can set the threshold for the element separately
71 |
72 | ```ts
73 | const el = document.getElementById('el')
74 | const exposure = createExposure(0.2)
75 |
76 | exposure.observe(el, () => {
77 | console.log('exposure')
78 | }, 0.8)
79 |
80 | ```
81 |
82 | > Needs attention:Element threshold > Exposure threshold
83 |
84 |
85 | ### Handler
86 | Handler has two types: function or object
87 |
88 | **Function**
89 |
90 | The function type is the more common way of writing, the function Handler will only be triggered once when the element is exposed and the `threshold` is met.
91 |
92 | **Object**
93 |
94 | A Handler of object type needs to have one of the `enter` and `leave` attributes, and the values of the `enter` and `leave` attributes are of function type.
95 |
96 | - enter: enter Handler is triggered once when an element enters exposure and `threshold` is met.
97 | - leave: the leave Handler is triggered once after the enter Handler is triggered and the element leaves the visible area completely.
98 |
99 |
100 | ### resetExposure
101 |
102 | Exposure callbacks are executed in a single instance, which means that once an exposure has been made and the callback executed, the callback function will not be executed again. If you need to expose again, you need to call `resetExposure` to reset it.
103 |
104 | ```ts
105 | import { resetExposure } from '@exposure-lib/core'
106 | // reset all elements
107 | resetExposure()
108 | // reset el element
109 | const el = document.getElementById('el')
110 | resetExposure(el)
111 | ```
112 |
113 | ### unobserve
114 |
115 | When the page is destroyed and the listener element in the current page needs to be unobserved, call the `exposure.unobserve` method to unobserve the listener element.
116 |
117 | ```ts
118 | const el = document.getElementById('el')
119 | const exposure = createExposure(0.2)
120 |
121 | exposure.observe(el, () => {
122 | console.log('exposure')
123 | }, 0.8)
124 |
125 | // Page Destroy
126 | destory(() => {
127 | exposure.unobserve(el)
128 | })
129 | ```
130 | ### Cautions
131 |
132 | exposure-lib listens to elements in strict mode, when an element's `visibility` is `hidden` or `width` is `0` or `height` is `0` it will not be listened to.
133 |
--------------------------------------------------------------------------------
/packages/vue/README.md:
--------------------------------------------------------------------------------
1 | # @exposure-lib/vue
2 |
3 | [@exposure-lib/vue 中文文档](./README.zh-CN.md)
4 |
5 | [@exposure-lib/core](../../README.md)
6 |
7 | Based on @exposure-lib/core, using vue directives to bind elements and execute callbacks when they appear in the viewport, supporting `Vue 3.x`.
8 |
9 | ## Quick Start
10 |
11 | ### Install
12 |
13 | ```shell
14 | pnpm add @exposure-lib/vue
15 | ```
16 |
17 | If the required browser environment does not support the `InterfaceObserver API`, then `ployfill` can be introduced for normal use
18 |
19 | ```shell
20 | pnpm add @exposure-lib/polyfill
21 | ```
22 |
23 | **Introducing the package**
24 |
25 | ```js
26 | import '@exposure-lib/polyfill'
27 | import Exposure from '@exposure-lib/vue'
28 | ```
29 |
30 | ### Using Plugin
31 |
32 | The @exposure-lib/vue callback function is executed by default when all areas of the element are displayed in the viewport.
33 |
34 | ```js
35 | createApp(App).use(Exposure).mount('#app')
36 | ```
37 |
38 |
39 | ### In Component
40 |
41 | @exposure-lib/vue is based on the vue directive wrapper, making it easier to develop components such as the one below.
42 |
43 | ```vue
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
77 |
78 |
91 | ```
92 |
93 | Scrolls the interface and triggers the callback function when the element appears in the viewport.
94 |
95 |
96 | #### Handler
97 | For details, see[exposure-lib](../../README.md)
98 | #### threshold
99 |
100 | By default, the execution of the exposure callback waits for the entire bound element to be fully wrapped before it is executed. If you have a need to expose an element when a certain percentage of it appears, the
101 | you can set the threshold, using the following two methods.
102 |
103 | ##### Golbal threshold
104 |
105 | @exposure-lib/vue supports global threshold settings.
106 |
107 | ```js
108 | Vue.use(Exposure, {
109 | threshold: 0.2,
110 | })
111 | ```
112 |
113 | As shown in the code above, the callback function is executed when the exposure ratio of the element reaches 0.2.
114 |
115 | ##### Element threshold
116 |
117 | If you want the exposure ratio of an element to be different from that of other elements, you can set the threshold for the element separately.
118 |
119 | ```vue
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
138 | ```
139 |
140 | Using Vue dynamic directive parameters for directives, the value passed must be a value between `[0,1]` so that the exposure will be in proportion to the value passed when listening to the exposure.
141 |
142 | > Needs attention: Golbal threshold > Element threshold
143 |
144 | ### useResetExposure
145 |
146 | Exposure callbacks are executed in a single instance, which means that once an exposure has been made and the callback executed, the callback function will not be executed again after another exposure.
147 |
148 | There is a KeepAlive scenario in Vue components, where the exposure callback is not re-executed when the KeepAlive component is switched. In this case, if you want to re-execute it, you need to use the `useResetExposure` API to reset the element state.
149 |
150 | ```js
151 | export default defineComponent({
152 | name: 'KeepaliveExposure',
153 | setup (props, context) {
154 | onDeactivated(() => {
155 | useResetExposure()
156 | })
157 | }
158 | })
159 | ```
160 |
161 | When calling `useResetExposure()` without passing any arguments, it will reset the execution state of all the listened elements in the current instance. If you need to reset the execution state of only one element, you need to pass in the current element.
162 |
163 | ```js
164 | export default defineComponent({
165 | name: 'KeepaliveExposure',
166 | setup(props, context) {
167 | onDeactivated(() => {
168 | useResetExposure(element)
169 | })
170 | },
171 | })
172 | ```
173 |
--------------------------------------------------------------------------------
/packages/vue2/README.md:
--------------------------------------------------------------------------------
1 | # @exposure-lib/vue2
2 |
3 | [@exposure-lib/vue2 中文文档](./README.zh-CN.md)
4 |
5 | [@exposure-lib/core](../../README.md)
6 |
7 | Based on @exposure-lib/core, using vue directives to bind elements and execute callbacks when they appear in the viewport, supporting `Vue 2.x`.
8 |
9 | ## Quick Start
10 |
11 | ### Install
12 |
13 | ```shell
14 | pnpm add @exposure-lib/vue2
15 | ```
16 |
17 | If the required browser environment does not support the `InterfaceObserver API`, then `ployfill` can be introduced for normal use
18 |
19 | ```shell
20 | pnpm add @exposure-lib/polyfill
21 | ```
22 |
23 | **Introducing the package**
24 |
25 | ```js
26 | import '@exposure-lib/polyfill'
27 | import Exposure from '@exposure-lib/vue2'
28 | ```
29 |
30 | ### Using Plugin
31 |
32 | The @exposure-lib/vue2 callback function is executed by default when all areas of the element are displayed in the viewport.
33 |
34 | ```js
35 | Vue.use(Exposure)
36 | ```
37 |
38 |
39 | ### In Component
40 |
41 | @exposure-lib/vue2 is based on the vue directive wrapper, making it easier to develop components such as the one below.
42 |
43 | ```vue
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
77 |
78 |
91 | ```
92 |
93 | Scrolls the interface and triggers the callback function when the element appears in the viewport.
94 |
95 |
96 | #### Handler
97 | For details, see[exposure-lib](../../README.md)
98 | #### threshold
99 |
100 | By default, the execution of the exposure callback waits for the entire bound element to be fully wrapped before it is executed. If you have a need to expose an element when a certain percentage of it appears, the
101 | you can set the threshold, using the following two methods.
102 |
103 | ##### Golbal threshold
104 |
105 | @exposure-lib/vue2 supports global threshold settings.
106 |
107 | ```js
108 | Vue.use(Exposure, {
109 | threshold: 0.2,
110 | })
111 | ```
112 |
113 | As shown in the code above, the callback function is executed when the exposure ratio of the element reaches 0.2.
114 |
115 | ##### Element threshold
116 |
117 | If you want the exposure ratio of an element to be different from that of other elements, you can set the threshold for the element separately.
118 |
119 | ```vue
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
138 | ```
139 |
140 | Using Vue dynamic directive parameters for directives, the value passed must be a value between `[0,1]` so that the exposure will be in proportion to the value passed when listening to the exposure.
141 |
142 | > Needs attention: Golbal threshold > Element threshold
143 |
144 | ### \$resetExposure
145 |
146 | Exposure callbacks are executed in a single instance, which means that once an exposure has been made and the callback executed, the callback function will not be executed again after another exposure.
147 |
148 | There is a KeepAlive scenario in Vue components, where the exposure callback is not re-executed when the KeepAlive component is switched. In this case, if you want to re-execute it, you need to use the `$resetExposure` API to reset the element state.
149 |
150 | ```js
151 | deactivated() {
152 | this.$resetExposure()
153 | }
154 | ```
155 |
156 | When calling `this.$resetExposure()` without passing any arguments, it will reset the execution state of all the listened elements in the current instance. If you need to reset the execution state of only one element, you need to pass in the current element.
157 |
158 | ```js
159 | deactivated() {
160 | this.$resetExposure(this.$refs.el)
161 | }
162 | ```
163 |
164 | #### Vue 2 + composition-api
165 | If the project is built with Vue 2 + composition-api, you can use useResetExposure to reset the exposure in order to follow the composition-api coding specification.
166 |
167 | ```ts
168 | import { useResetExposure } from 'vue-exposure'
169 | import { defineComponent, onDeactivated } from '@vue/composition-api'
170 | export default defineComponent({
171 | setup() {
172 | onDeactivated(() => {
173 | useResetExposure()
174 | })
175 | }
176 | })
177 | ```
178 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises
2 | const inquirer = require('inquirer')
3 | const typescript = require('rollup-plugin-typescript2')
4 | const chalk = require('chalk')
5 | const rollup = require('rollup')
6 | const { resolve } = require('path')
7 |
8 | const args = require('minimist')(process.argv.slice(2))
9 |
10 | const getPackagesName = async () => {
11 | const allPackagesName = await fs.readdir(resolve(__dirname, '../packages'))
12 | return allPackagesName
13 | .filter((packageName) => {
14 | const isHiddenFile = /^\./g.test(packageName)
15 | return !isHiddenFile
16 | })
17 | .filter((packageName) => {
18 | const isPrivatePackage = require(resolve(
19 | __dirname,
20 | `../packages/${packageName}/package.json`
21 | )).private
22 | return !isPrivatePackage
23 | })
24 | }
25 |
26 | const getAnswersFromInquirer = async (packagesName) => {
27 | const choicePackageQuestion = {
28 | type: 'checkbox',
29 | name: 'packages',
30 | scroll: false,
31 | message: 'Select build repo(Support Multiple selection)',
32 | choices: packagesName.map((packageName) => ({
33 | value: packageName,
34 | packageName,
35 | })),
36 | }
37 | let { packages } = await inquirer.prompt(choicePackageQuestion)
38 | if (!packages.length) {
39 | console.log(
40 | chalk.yellow(`
41 | It seems that you did't make a choice.
42 |
43 | Please try it again.
44 | `)
45 | )
46 | return
47 | }
48 | if (packages.some((package) => package === 'all')) {
49 | packagesName.shift()
50 | packages = packagesName
51 | }
52 | const confirmPackageQuestion = {
53 | name: 'confirm',
54 | message: `Confirm build ${packages.join(' and ')} packages?`,
55 | type: 'list',
56 | choices: ['Y', 'N'],
57 | }
58 | const { confirm } = await inquirer.prompt(confirmPackageQuestion)
59 | if (confirm === 'N') {
60 | console.log(chalk.yellow('[release] cancelled.'))
61 | return
62 | }
63 | return packages
64 | }
65 |
66 | const cleanPackagesOldDist = async (packagesName) => {
67 | for (let packageName of packagesName) {
68 | const distPath = resolve(__dirname, `../packages/${packageName}/dist`)
69 | try {
70 | const stat = await fs.stat(distPath)
71 | if (stat.isDirectory()) {
72 | await fs.rm(distPath, {
73 | recursive: true,
74 | })
75 | }
76 | } catch (err) {
77 | console.log('err', err)
78 | console.log(chalk.red(`remove ${packageName} dist dir error!`))
79 | }
80 | }
81 | }
82 |
83 | const cleanPackagesDtsDir = async (packageName) => {
84 | const dtsPath = resolve(__dirname, `../packages/${packageName}/dist/packages`)
85 | console.log('dtsPath', dtsPath)
86 | try {
87 | const stat = await fs.stat(dtsPath)
88 | if (stat.isDirectory()) {
89 | await fs.rm(dtsPath, {
90 | recursive: true,
91 | })
92 | }
93 | } catch (err) {
94 | console.log(err)
95 | console.log(chalk.red(`remove ${packageName} dist/packages dir error!`))
96 | }
97 | }
98 |
99 | const pascalCase = (str) => {
100 | const re = /-(\w)/g
101 | const newStr = str.replace(re, function (match, group1) {
102 | return group1.toUpperCase()
103 | })
104 | return newStr.charAt(0).toUpperCase() + newStr.slice(1)
105 | }
106 |
107 | const formats = ['esm', 'cjs']
108 | const packageOtherConfig = {
109 | vue2: {
110 | external: ['@exposure-lib/core'],
111 | },
112 | vue: {
113 | external: ['@exposure-lib/core'],
114 | },
115 | }
116 | const generateBuildConfigs = (packagesName) => {
117 | const packagesFormatConfig = packagesName.map((packageName) => {
118 | const formatConfigs = []
119 | for (let format of formats) {
120 | formatConfigs.push({
121 | packageName,
122 | config: {
123 | input: resolve(__dirname, `../packages/${packageName}/src/index.ts`),
124 | output: {
125 | name: pascalCase(packageName),
126 | file: resolve(
127 | __dirname,
128 | `../packages/${packageName}/dist/index.${format}.js`
129 | ),
130 | format,
131 | },
132 | plugins: [
133 | typescript({
134 | verbosity: -1,
135 | tsconfig: resolve(__dirname, '../tsconfig.json'),
136 | tsconfigOverride: {
137 | include: [`package/${packageName}/src`],
138 | },
139 | }),
140 | ],
141 | ...packageOtherConfig[packageName],
142 | },
143 | })
144 | }
145 | return formatConfigs
146 | })
147 | return packagesFormatConfig.flat()
148 | }
149 |
150 | const extractDts = (packageName) => {
151 | const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor')
152 | const extractorConfigPath = resolve(
153 | __dirname,
154 | `../packages/${packageName}/api-extractor.json`
155 | )
156 | const extractorConfig =
157 | ExtractorConfig.loadFileAndPrepare(extractorConfigPath)
158 | const result = Extractor.invoke(extractorConfig, {
159 | localBuild: true,
160 | showVerboseMessages: true,
161 | })
162 | return result
163 | }
164 |
165 | const buildEntry = async (packageConfig) => {
166 | try {
167 | const packageBundle = await rollup.rollup(packageConfig.config)
168 | await packageBundle.write(packageConfig.config.output)
169 | const extractResult = extractDts(packageConfig.packageName)
170 | await cleanPackagesDtsDir(packageConfig.packageName)
171 | if (!extractResult.succeeded) {
172 | console.log(chalk.red(`${packageConfig.packageName} d.ts extract fail!`))
173 | }
174 | console.log(chalk.green(`${packageConfig.packageName} build successful! `))
175 | } catch (err) {
176 | console.log(chalk.red(`${packageConfig.packageName} build fail!`))
177 | }
178 | }
179 |
180 | const build = async (packagesConfig) => {
181 | for (let config of packagesConfig) {
182 | await buildEntry(config)
183 | }
184 | }
185 |
186 | const buildBootstrap = async () => {
187 | const packagesName = await getPackagesName()
188 | let buildPackagesName = packagesName
189 | if (!args.all) {
190 | packagesName.unshift('all')
191 | const answers = await getAnswersFromInquirer(packagesName)
192 | if (!answers) {
193 | return
194 | }
195 | buildPackagesName = answers
196 | }
197 | await cleanPackagesOldDist(buildPackagesName)
198 | const packagesBuildConfig = generateBuildConfigs(buildPackagesName)
199 | await build(packagesBuildConfig)
200 | }
201 |
202 | buildBootstrap().catch((err) => {
203 | console.log('err', err)
204 | process.exit(1)
205 | })
206 |
--------------------------------------------------------------------------------
/packages/polyfill/lib/polyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
5 | *
6 | * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
7 | *
8 | */
9 | ;(function () {
10 | 'use strict'
11 |
12 | // Exit early if we're not running in a browser.
13 | if (typeof window !== 'object') {
14 | return
15 | }
16 |
17 | // Exit early if all IntersectionObserver and IntersectionObserverEntry
18 | // features are natively supported.
19 | if (
20 | 'IntersectionObserver' in window &&
21 | 'IntersectionObserverEntry' in window &&
22 | 'intersectionRatio' in window.IntersectionObserverEntry.prototype
23 | ) {
24 | // Minimal polyfill for Edge 15's lack of `isIntersecting`
25 | // See: https://github.com/w3c/IntersectionObserver/issues/211
26 | if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
27 | Object.defineProperty(
28 | window.IntersectionObserverEntry.prototype,
29 | 'isIntersecting',
30 | {
31 | get: function () {
32 | return this.intersectionRatio > 0
33 | },
34 | }
35 | )
36 | }
37 | return
38 | }
39 |
40 | /**
41 | * A local reference to the document.
42 | */
43 | var document = window.document
44 |
45 | /**
46 | * An IntersectionObserver registry. This registry exists to hold a strong
47 | * reference to IntersectionObserver instances currently observing a target
48 | * element. Without this registry, instances without another reference may be
49 | * garbage collected.
50 | */
51 | var registry = []
52 |
53 | /**
54 | * The signal updater for cross-origin intersection. When not null, it means
55 | * that the polyfill is configured to work in a cross-origin mode.
56 | * @type {function(DOMRect|ClientRect, DOMRect|ClientRect)}
57 | */
58 | var crossOriginUpdater = null
59 |
60 | /**
61 | * The current cross-origin intersection. Only used in the cross-origin mode.
62 | * @type {DOMRect|ClientRect}
63 | */
64 | var crossOriginRect = null
65 |
66 | /**
67 | * Creates the global IntersectionObserverEntry constructor.
68 | * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
69 | * @param {Object} entry A dictionary of instance properties.
70 | * @constructor
71 | */
72 | function IntersectionObserverEntry(entry) {
73 | this.time = entry.time
74 | this.target = entry.target
75 | this.rootBounds = ensureDOMRect(entry.rootBounds)
76 | this.boundingClientRect = ensureDOMRect(entry.boundingClientRect)
77 | this.intersectionRect = ensureDOMRect(
78 | entry.intersectionRect || getEmptyRect()
79 | )
80 | this.isIntersecting = !!entry.intersectionRect
81 |
82 | // Calculates the intersection ratio.
83 | var targetRect = this.boundingClientRect
84 | var targetArea = targetRect.width * targetRect.height
85 | var intersectionRect = this.intersectionRect
86 | var intersectionArea = intersectionRect.width * intersectionRect.height
87 |
88 | // Sets intersection ratio.
89 | if (targetArea) {
90 | // Round the intersection ratio to avoid floating point math issues:
91 | // https://github.com/w3c/IntersectionObserver/issues/324
92 | this.intersectionRatio = Number(
93 | (intersectionArea / targetArea).toFixed(4)
94 | )
95 | } else {
96 | // If area is zero and is intersecting, sets to 1, otherwise to 0
97 | this.intersectionRatio = this.isIntersecting ? 1 : 0
98 | }
99 | }
100 |
101 | /**
102 | * Creates the global IntersectionObserver constructor.
103 | * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
104 | * @param {Function} callback The function to be invoked after intersection
105 | * changes have queued. The function is not invoked if the queue has
106 | * been emptied by calling the `takeRecords` method.
107 | * @param {Object=} opt_options Optional configuration options.
108 | * @constructor
109 | */
110 | function IntersectionObserver(callback, opt_options) {
111 | var options = opt_options || {}
112 |
113 | if (typeof callback != 'function') {
114 | throw new Error('callback must be a function')
115 | }
116 |
117 | if (options.root && options.root.nodeType != 1) {
118 | throw new Error('root must be an Element')
119 | }
120 |
121 | // Binds and throttles `this._checkForIntersections`.
122 | this._checkForIntersections = throttle(
123 | this._checkForIntersections.bind(this),
124 | this.THROTTLE_TIMEOUT
125 | )
126 |
127 | // Private properties.
128 | this._callback = callback
129 | this._observationTargets = []
130 | this._queuedEntries = []
131 | this._rootMarginValues = this._parseRootMargin(options.rootMargin)
132 |
133 | // Public properties.
134 | this.thresholds = this._initThresholds(options.threshold)
135 | this.root = options.root || null
136 | this.rootMargin = this._rootMarginValues
137 | .map(function (margin) {
138 | return margin.value + margin.unit
139 | })
140 | .join(' ')
141 |
142 | /** @private @const {!Array} */
143 | this._monitoringDocuments = []
144 | /** @private @const {!Array} */
145 | this._monitoringUnsubscribes = []
146 | }
147 |
148 | /**
149 | * The minimum interval within which the document will be checked for
150 | * intersection changes.
151 | */
152 | IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100
153 |
154 | /**
155 | * The frequency in which the polyfill polls for intersection changes.
156 | * this can be updated on a per instance basis and must be set prior to
157 | * calling `observe` on the first target.
158 | */
159 | IntersectionObserver.prototype.POLL_INTERVAL = null
160 |
161 | /**
162 | * Use a mutation observer on the root element
163 | * to detect intersection changes.
164 | */
165 | IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true
166 |
167 | /**
168 | * Sets up the polyfill in the cross-origin mode. The result is the
169 | * updater function that accepts two arguments: `boundingClientRect` and
170 | * `intersectionRect` - just as these fields would be available to the
171 | * parent via `IntersectionObserverEntry`. This function should be called
172 | * each time the iframe receives intersection information from the parent
173 | * window, e.g. via messaging.
174 | * @return {function(DOMRect|ClientRect, DOMRect|ClientRect)}
175 | */
176 | IntersectionObserver._setupCrossOriginUpdater = function () {
177 | if (!crossOriginUpdater) {
178 | /**
179 | * @param {DOMRect|ClientRect} boundingClientRect
180 | * @param {DOMRect|ClientRect} intersectionRect
181 | */
182 | crossOriginUpdater = function (boundingClientRect, intersectionRect) {
183 | if (!boundingClientRect || !intersectionRect) {
184 | crossOriginRect = getEmptyRect()
185 | } else {
186 | crossOriginRect = convertFromParentRect(
187 | boundingClientRect,
188 | intersectionRect
189 | )
190 | }
191 | registry.forEach(function (observer) {
192 | observer._checkForIntersections()
193 | })
194 | }
195 | }
196 | return crossOriginUpdater
197 | }
198 |
199 | /**
200 | * Resets the cross-origin mode.
201 | */
202 | IntersectionObserver._resetCrossOriginUpdater = function () {
203 | crossOriginUpdater = null
204 | crossOriginRect = null
205 | }
206 |
207 | /**
208 | * Starts observing a target element for intersection changes based on
209 | * the thresholds values.
210 | * @param {Element} target The DOM element to observe.
211 | */
212 | IntersectionObserver.prototype.observe = function (target) {
213 | var isTargetAlreadyObserved = this._observationTargets.some(function (
214 | item
215 | ) {
216 | return item.element == target
217 | })
218 |
219 | if (isTargetAlreadyObserved) {
220 | return
221 | }
222 |
223 | if (!(target && target.nodeType == 1)) {
224 | throw new Error('target must be an Element')
225 | }
226 |
227 | this._registerInstance()
228 | this._observationTargets.push({ element: target, entry: null })
229 | this._monitorIntersections(target.ownerDocument)
230 | this._checkForIntersections()
231 | }
232 |
233 | /**
234 | * Stops observing a target element for intersection changes.
235 | * @param {Element} target The DOM element to observe.
236 | */
237 | IntersectionObserver.prototype.unobserve = function (target) {
238 | this._observationTargets = this._observationTargets.filter(function (item) {
239 | return item.element != target
240 | })
241 | this._unmonitorIntersections(target.ownerDocument)
242 | if (this._observationTargets.length == 0) {
243 | this._unregisterInstance()
244 | }
245 | }
246 |
247 | /**
248 | * Stops observing all target elements for intersection changes.
249 | */
250 | IntersectionObserver.prototype.disconnect = function () {
251 | this._observationTargets = []
252 | this._unmonitorAllIntersections()
253 | this._unregisterInstance()
254 | }
255 |
256 | /**
257 | * Returns any queue entries that have not yet been reported to the
258 | * callback and clears the queue. This can be used in conjunction with the
259 | * callback to obtain the absolute most up-to-date intersection information.
260 | * @return {Array} The currently queued entries.
261 | */
262 | IntersectionObserver.prototype.takeRecords = function () {
263 | var records = this._queuedEntries.slice()
264 | this._queuedEntries = []
265 | return records
266 | }
267 |
268 | /**
269 | * Accepts the threshold value from the user configuration object and
270 | * returns a sorted array of unique threshold values. If a value is not
271 | * between 0 and 1 and error is thrown.
272 | * @private
273 | * @param {Array|number=} opt_threshold An optional threshold value or
274 | * a list of threshold values, defaulting to [0].
275 | * @return {Array} A sorted list of unique and valid threshold values.
276 | */
277 | IntersectionObserver.prototype._initThresholds = function (opt_threshold) {
278 | var threshold = opt_threshold || [0]
279 | if (!Array.isArray(threshold)) threshold = [threshold]
280 |
281 | return threshold.sort().filter(function (t, i, a) {
282 | if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
283 | throw new Error(
284 | 'threshold must be a number between 0 and 1 inclusively'
285 | )
286 | }
287 | return t !== a[i - 1]
288 | })
289 | }
290 |
291 | /**
292 | * Accepts the rootMargin value from the user configuration object
293 | * and returns an array of the four margin values as an object containing
294 | * the value and unit properties. If any of the values are not properly
295 | * formatted or use a unit other than px or %, and error is thrown.
296 | * @private
297 | * @param {string=} opt_rootMargin An optional rootMargin value,
298 | * defaulting to '0px'.
299 | * @return {Array