├── .babelrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── LICENSE ├── README.md ├── README.zh-Hans.md ├── README.zh-Hant.md ├── dist └── bundle.js ├── index.html ├── package-lock.json ├── package.json ├── prettier.config.js ├── readme-assets └── tg-edge-explaination.png ├── src ├── bind.ts ├── directives │ ├── index.ts │ ├── tg-bezier.ts │ ├── tg-edge.ts │ ├── tg-filter.ts │ ├── tg-follow.ts │ ├── tg-from.ts │ ├── tg-map.ts │ ├── tg-name.ts │ ├── tg-ref.ts │ ├── tg-step.ts │ ├── tg-steps.ts │ └── tg-to.ts ├── ease.ts ├── helpers.ts ├── index.ts ├── observer.ts ├── parse.ts ├── prefix.ts ├── trigger.ts └── type.ts ├── tea.yaml ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | registry-url: https://registry.npmjs.org/ 26 | 27 | - name: Cache node_modules 28 | uses: actions/cache@v2 29 | env: 30 | cache-name: build-prod 31 | with: 32 | # npm cache files are stored in `~/.npm` on Linux/macOS 33 | path: ~/.npm 34 | key: ${{ runner.os }}-node-cache-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: | 36 | ${{ runner.os }}-node-cache-${{ env.cache-name }}- 37 | 38 | - name: Install dependencies 39 | run: npm ci 40 | 41 | - name: Build for production 42 | run: npm run prod 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021, Wai Kin (Steven) Lei 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trigger JS 2 | 3 | [![Build Status](https://github.com/triggerjs/trigger/actions/workflows/build.yml/badge.svg)](https://github.com/triggerjs/trigger/blob/main/.github/workflows/build.yml) 4 | [![npm version](https://img.shields.io/npm/v/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 5 | [![npm downloads](https://img.shields.io/npm/dm/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 6 | ![GitHub Stars](https://img.shields.io/github/stars/triggerjs/trigger) 7 | ![Github Forks](https://img.shields.io/github/forks/triggerjs/trigger) 8 | ![GitHub Open Issues](https://img.shields.io/github/issues/triggerjs/trigger) 9 | ![License](https://img.shields.io/github/license/triggerjs/trigger) 10 | 11 | *Create scroll-based animation without JavaScript.* 12 | 13 | Sometimes we want to update the CSS style of an HTML element based on the scroll position, just like fast-forwarding or rewinding a video by scrolling up and down. 14 | 15 | With Trigger JS, get the desired value with CSS variable on page scroll for your animation needed, without writing a single line of JavaScript code, configuration with HTML attributes. Checkout [examples here](https://codepen.io/collection/eJmoMr). 16 | 17 | Read this document in other languages: [English](README.md), [繁體中文](README.zh-Hant.md), [简体中文](README.zh-Hans.md). 18 | 19 | ## Getting Started 20 | 21 | #### Method 1: Via CDN 22 | 23 | 1. Include Trigger JS to your webpage with a script tag, with either CDN: 24 | - UNPKG CDN: 25 | ```html 26 | 27 | ``` 28 | - jsDelivr CDN: 29 | ```html 30 | 31 | ``` 32 | 33 | 2. Add `tg-name` to the DOM element that you want to monitor. The value of `tg-name` is the name of the CSS variable that binds to the element. 34 | 35 | ```html 36 |
Hello, World
37 | ``` 38 | 39 | In the above example, CSS variable `--scrolled` is added to the selector `#greeting`: 40 | 41 | ```html 42 | 53 | ``` 54 | 55 | 3. Scroll the page and see the result. 56 | 57 | #### Method 2: Build from source 58 | 59 | 1. Get the library in either way: 60 | - From GitHub 61 | ```bash 62 | git clone https://github.com/triggerjs/trigger.git 63 | ``` 64 | - From NPM 65 | ```bash 66 | npm i @triggerjs/trigger 67 | ``` 68 | 2. Change to the directory, install the dependencies: 69 | ```bash 70 | npm install 71 | ``` 72 | 3. There is a pre-built version `bundle.js` located in `dist`. Run a local web server and browse the greeting example in `index.html` : 73 | 1. For example, type `npx serve` in the terminal 74 | 2. Open up `http://localhost:5000` in web browser. 75 | 3. Scroll the page and see the result. 76 | 4. The following command will build a new version to `dist/bundle.js`: 77 | - For development (with watch): 78 | ```bash 79 | npm run watch 80 | ``` 81 | - For development: 82 | ```bash 83 | npm run build 84 | ``` 85 | - For production: 86 | ```bash 87 | npm run prod 88 | ``` 89 | 90 | ## The `tg-` Attributes 91 | 92 | | Attribute | Type | Default | Description | 93 | | ----------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 94 | | `tg-name` | Required | - | The CSS variable name to store the value, with or without `--` prefix. | 95 | | `tg-from` | Optional | `0` | The start value | 96 | | `tg-to` | Optional | `1` | The end value | 97 | | `tg-steps` | Optional | `100` | Steps to be triggered from `tg-from` to `tg-to` | 98 | | `tg-step` | Optional | `0` | Step per increment. If this value isn't `0`, will override `tg-steps`. | 99 | | `tg-map` | Optional | (Empty) | Map the value to another value. Format:
- 1-to-1 mapping: `value: newValue; value2: newValue2`.
- Multiple-to-1 mapping: `value,value2,value3: newValue`.
- Range-to-1 mapping: `value...value2: newValue`. | 100 | | `tg-filter` | Optional | (Empty) | Only trigger if the scroll value is on the list. Format: `1,3,5,7,9`. By default, the filter mode is `retain`. If we want to switch the mode to `exact`, add an `!` at the end of the value. Read more about this in the dedicated section following. | 101 | | `tg-edge` | Optional | cover | Calculate the start and end of the scrolling effect. `cover` means off-screen to off-screen. The calculation starts in the appearance of the element at the bottom, and ends in the disappearance of element at the top; `inset` represents the calculation begins after the top edge of the element touches the top of the screen, ends when the bottom edge of the element reached the bottom of the screen. See below section for a diagram. | 102 | | `tg-follow` | Optional | (Empty) | Use the result calculated from another element. The value of `tg-follow` is the value of the target element's `tg-ref`. **Caution**: When `tg-follow` is set, `tg-from`, `tg-to`, `tg-steps`, `tg-step` and `tg-edge` are ignored in the same element. | 103 | | `tg-ref` | Optional | (Empty) | Define the name for other elements to reference using `tg-follow`. | 104 | | `tg-bezier` | Optional | (Empty) | Bezier easing setting, available values: `ease`, `easeIn`, `easeOut`, `easeInOut`, or custom numbers for a Cubic Bezier in format `p1x,p1y,p2x,p2y`. 105 | 106 | ## Value Mapping 107 | 108 | Number is not suitable for all the situations. For example, we want to update the text color based on the scroll value. the attribute `tg-map` can help. 109 | 110 | The following example shows how to update the text color with the rules below: 111 | | Element Position (From the Bottom) | Scroll Value | Text Color | 112 | | ---------------------------------- | --------------- | ---------- | 113 | | 0% - 10% | 1 | black | 114 | | 10% - 20% | 2 | red | 115 | | 20% - 30% | 3 | orange | 116 | | 30% - 40% | 4 | yellow | 117 | | 40% - 50% | 5 | green | 118 | | 50% - 60% | 6 | cyan | 119 | | 60% - 70% | 7 | blue | 120 | | 70% - 80% | 8 | purple | 121 | | 80% - 90% | 9 | grey | 122 | | 90% - 100% | 10 | grey | 123 | 124 | ```html 125 |

133 | Rainbow Text 134 |

135 | 136 | 145 | ``` 146 | 147 | ## Steps & Step 148 | 149 | Let's say `tg-from="200"` and `tg-to="-200"`, we want to move the element in x position with `transform: translateX()`. `tg-steps` lets us define how many steps from `200` to `-200`, for example, `tg-steps="400"` means run from `200` to `-200` with `400` steps, `1` per increment; In other words, `tg-steps="800"` means `0.5` per increment. 150 | 151 | But sometimes, we do not want to do the math by ourselves, that's why `tg-step` exists. `tg-step` defines the exact value of increment. Please note that if `tg-step` is defined, `tg-steps` will be ignored. 152 | 153 | ## Noise Reduction 154 | 155 | Sometimes we are only interested in certain values. For example, we only want to know when `25, 50, 75` show up from `0` to `100` (`tg-from="0"` and `tg-to="100"`). In this situation, `tg-filter` helps you. 156 | 157 | ```html 158 |

167 | Red (25), Yellow (50), Green (75) 168 |

169 | 170 | 179 | ``` 180 | 181 | ## The mode of `tg-filter` 182 | 183 | There are two modes for `tg-filter`, `retain` by default, the other one is `exact`. Here is an example to clarify this: 184 | 185 | ```html 186 |

195 | Trigger.js 196 |

197 | 198 | 208 | ``` 209 | 210 | In the above example, the text has an initial color of black, and it will turn to blue when it arrives at the middle of the page and never turn to black again because there is no trigger point of the black color. 211 | 212 | So let's say we want the text color becomes blue only when the calculation value is `5`, and becomes black for other values, We can change it to: 213 | 214 | ```html 215 |

224 | Trigger.js 225 |

226 | ``` 227 | 228 | It works, but the code becomes redundant. To solve this, we can switch the filter mode to `exact` by adding an `!` at the end of the value of `tg-filter`: 229 | 230 | ```html 231 |

240 | Trigger.js 241 |

242 | ``` 243 | 244 | In `exact` mode, `--color` becomes `blue` when the value is `5`, and becomes the default when the value is not `5`. 245 | 246 | The design of adding `!` to the value of `tg-filter` is the demand is exclusive to the attribute. Establishing another attribute for the mode is unnecessary or even leads to the misunderstanding. 247 | 248 | ## Value Inheritance 249 | 250 | Just like some CSS properties, the values of `tg-` attributes (except `tg-follow`, `tg-ref`) inherits from the parents if not being set in the current element. If we do not want it inherits from parent and set it as default value, just add the `tg-` attribute without value. For example: 251 | 252 | ```html 253 |
254 | 255 | 256 | 257 |
258 | ``` 259 | 260 | ## `tg-edge` Explaination 261 | 262 | The different between `cover` (default) and `edge`: 263 | 264 | ![](/readme-assets/tg-edge-explaination.png) 265 | 266 | So that if `tg-edge="inset"`, the element must be higher than the viewport (`window.clientHeight`). 267 | 268 | ## JavaScript Event 269 | 270 | We can also listen to the `tg` event on an element with JavaScript: 271 | 272 | ```html 273 |

281 | Trigger JS 282 |

283 | 284 | 293 | 294 | 299 | ``` 300 | 301 | ## Customising the Prefix 302 | 303 | If you are concerned with the `tg-` prefix that doesn't quite fulfill the standard of HTML5, it can be customised by the following setting in the `body` tag with `data-trigger-prefix` attribute: 304 | 305 | ```html 306 | 307 |
Hello, World
308 | 309 | ``` 310 | 311 | The above example customises the prefix to `data-tg`. `data-*` is a completely valid attribute for putting custom data in HTML5. 312 | 313 | ## Contribute 314 | 315 | Feel free to fork this repository and submit pull requests. Bugs report in [GitHub Issues](https://github.com/triggerjs/trigger/issues), features/ideas/questions discuss in [GitHub Discussions](https://github.com/triggerjs/trigger/discussions). 316 | 317 | ## License 318 | 319 | Trigger.js is [MIT Licensed](LICENSE). 320 | -------------------------------------------------------------------------------- /README.zh-Hans.md: -------------------------------------------------------------------------------- 1 | # Trigger JS 2 | 3 | [![Build Status](https://github.com/triggerjs/trigger/actions/workflows/build.yml/badge.svg)](https://github.com/triggerjs/trigger/blob/main/.github/workflows/build.yml) 4 | [![npm version](https://img.shields.io/npm/v/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 5 | [![npm downloads](https://img.shields.io/npm/dm/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 6 | ![GitHub Stars](https://img.shields.io/github/stars/triggerjs/trigger) 7 | ![Github Forks](https://img.shields.io/github/forks/triggerjs/trigger) 8 | ![GitHub Open Issues](https://img.shields.io/github/issues/triggerjs/trigger) 9 | ![License](https://img.shields.io/github/license/triggerjs/trigger) 10 | 11 | *毋须编写 JavaScript,建立页面滚动动画。* 12 | 13 | 有时候我们想因应页面滚动的位置建立动画,或更新 HTML 元素的 CSS 样式。就像视频播放一样,动画在页面向上滚动时前进,向下滚动时后退。 14 | 15 | 使用 Trigger JS,在页面滚动时即可透过 CSS 变量取得制作动画所需的值,而毋须编写任何 JavaScript 代码,相关设置则可通过 HTML 属性实现。[点击这里](https://codepen.io/collection/eJmoMr)查看范例。 16 | 17 | 阅读本文档的其他语言版本:[English](README.md)、[繁體中文](README.zh-Hant.md)、[简体中文](README.zh-Hans.md)。 18 | 19 | ## 如何使用 20 | 21 | #### 方法一:使用 CDN 版本 22 | 23 | 1. 通过 script 标签将 trigger.js 加载到网页中: 24 | - UNPKG CDN: 25 | ```html 26 | 27 | ``` 28 | - jsDelivr CDN: 29 | ```html 30 | 31 | ``` 32 | 33 | 2. 为对应的 DOM 元素加上 `tg-name` 属性,设定值等于接收数值的 CSS 变量名。 34 | 35 | ```html 36 |
Hello, World
37 | ``` 38 | 39 | 上述例子中,CSS 变量 `--scrolled` 会被加入到 `#greeting` 选择器中: 40 | 41 | ```html 42 | 51 | ``` 52 | 53 | 3. 滚动页面,测试效果。 54 | 55 | #### 方法二:从源代码打包 56 | 57 | 1. 从以下任一途径获取源代码: 58 | - GitHub 59 | ```bash 60 | git clone https://github.com/triggerjs/trigger.git 61 | ``` 62 | - NPM 63 | ```bash 64 | npm i @triggerjs/trigger 65 | ``` 66 | 2. 切换到目录内,安装依赖: 67 | ```bash 68 | npm install 69 | ``` 70 | 3. 本身已经有一个预打包的版本在 `dist/bundle.js`。运行一个本地网页服务器,查看范例欢迎页 `index.html`: 71 | 1. 例如通过运行 `npx serve` 72 | 2. 在浏览器中打开 `http://localhost:5000` 73 | 3. 滚动页面,测试效果。 74 | 4. 以下指令会打包一个新的版本到 `dist/bundle.js`: 75 | - 开发环境版本(持续监听修改): 76 | ```bash 77 | npm run watch 78 | ``` 79 | - 开发环境版本: 80 | ```bash 81 | npm run build 82 | ``` 83 | - 生产环境版本: 84 | ```bash 85 | npm run prod 86 | ``` 87 | 88 | ## `tg-` 属性列表 89 | 90 | | 属性名称 | 类型 | 默认值 | 简介 | 91 | | ----------- | ---- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 92 | | `tg-name` | 必填 | - | 接收滚动值的 CSS 变量名称,是否加上 `--` 前缀都可。 | 93 | | `tg-from` | 选填 | `0` | 起始值 | 94 | | `tg-to` | 选填 | `1` | 终点值 | 95 | | `tg-steps` | 选填 | `100` | 从 `tg-from` 至 `tg-to` 之间触发多少次 | 96 | | `tg-step` | 选填 | `0` | 每次递加的数值,如果此值不为 `0`,则会忽略 `tg-steps` 的设置。 | 97 | | `tg-map` | 选填 | (空白字符串) | 将一个值映射至另一个值。格式:
- 一个值映射至另一个值:`value: newValue; value2: newValue2`
- 多个值映射至另一个值:`value,value2,value3: newValue`
- 一个范围映射至另一个值:`value...value2: newValue` | 98 | | `tg-filter` | 选填 | (空白字符串) | 仅当滚动值在列表当中时才触发,格式:`1,3,5,7,9`。默认情况下,过滤模式是 `retain`(保留值),在设定值末端加入 `!` 符号可以将模式切换为 `exact`(绝对)。关于两个模式的区别,请参考后续的内容。 | 99 | | `tg-edge` | 选填 | cover | 计算滚动值的起始点与结束点。`cover` 代表画面外至画面外,即在元素从底部进入画面时开始计算,从顶部完整离开画面时结束;`inset` 代表当元素的顶部触及页面的顶部时开始计算,元素的底部触及页面的底部时结束。以下将有图解说明。 | 100 | | `tg-follow` | 选填 | (空白字符串) | 引用其他元素的计算值。`tg-follow` 的设定值等于目标元素的 `tg-ref` 设定值。**注意**:当设定了 `tg-follow`,同一元素下的 `tg-from`、`tg-to`、`tg-steps`、`tg-step` 以及 `tg-edge` 设置会被忽略。 | 101 | | `tg-ref` | 选填 | (空白字符串) | 定义可以被其他元素通过 `tg-follow` 引用的名称。 | 102 | | `tg-bezier` | 选填 | (空白字符串) | 贝塞尔曲线设定,设定值为 `ease`、`easeIn`、`easeOut`、`easeInOut` 或自定义的贝塞尔曲线数值,格式是:`p1x,p1y,p2x,p2y`。 | 103 | 104 | ## 映射 (Value Mapping) 105 | 106 | 数字并不适用于所有情况。例如,我们希望在滚动页面的时候更改文字颜色,这个时候 `tg-map` 属性就派上用场了。 107 | 108 | 以下例子示范如何根据下表的规则更新文字颜色: 109 | 110 | | 元素位置(从页底起) | 卷动的值 | 文字颜色 | 111 | | -------------------- | -------- | -------- | 112 | | 0% - 10% | 1 | black | 113 | | 10% - 20% | 2 | red | 114 | | 20% - 30% | 3 | orange | 115 | | 30% - 40% | 4 | yellow | 116 | | 40% - 50% | 5 | green | 117 | | 50% - 60% | 6 | cyan | 118 | | 60% - 70% | 7 | blue | 119 | | 70% - 80% | 8 | purple | 120 | | 80% - 90% | 9 | grey | 121 | | 90% - 100% | 10 | grey | 122 | 123 | ```html 124 |

132 | 彩虹文字 133 |

134 | 135 | 144 | ``` 145 | 146 | ## Steps & Step 147 | 148 | 假设 `tg-from="200"` 以及 `tg-to="-200"`,我们想通过 `transform: translateX()` 将一个元素在 x 方向移动。`tg-steps` 让我们设定从 `200` 到 `-200` 总共有多少步,举个例子,`tg-steps="400"` 等于用 `400` 步从 `200` 递减到 `-200`,每一步等于 `1`;换句话说,`tg-steps="800"` 就代表每一步等于 `0.5`。 149 | 150 | 但是有些时候,我们不想自行运算,这就是 `tg-step` 出现的原因。`tg-step` 直接定义每一步的值,所以如果定义了 `tg-step`,`tg-steps` 就会被忽略。 151 | 152 | ## 降噪 153 | 154 | 有时我们只对某些特定的值感兴趣。例如,我们只想知道从 `0` 至 `100` (`tg-from="0"` 以及 `tg-to="100"`) 之间,什么时候出现 `25, 50, 75`。在这个情况中,`tg-filter` 就可以帮上忙。 155 | 156 | ```html 157 |

166 | Red (25), Yellow (50), Green (75) 167 |

168 | 169 | 178 | ``` 179 | 180 | ## `tg-filter` 的模式 181 | 182 | `tg-filter` 有两个模式,默认是 `retain`,另一个设定值是 `exact`。为了更好的说明差异,请参阅以下例子: 183 | 184 | ```html 185 |

194 | Trigger.js 195 |

196 | 197 | 207 | ``` 208 | 209 | 上述例子中,文字颜色初始是黑色,而将文字卷动到页面中间时,会变为蓝色。不过文字就永远都不会变回黑色了,因为没有让它改变为黑色的触发点。 210 | 211 | 如果我们想文字颜色只在计算值是 `5` 的时候改变为蓝色,其他时候是黑色,可以将代码更改为: 212 | 213 | ```html 214 |

223 | Trigger.js 224 |

225 | ``` 226 | 227 | 这样虽然可以,不过很快我们的代码就变得冗长。为了解决这个情况,我们可以将模式切换为 `exact`,只须在 `tg-filter` 的末端加入 `!` 符号即可: 228 | 229 | ```html 230 |

239 | Trigger.js 240 |

241 | ``` 242 | 243 | 在 `exact` 模式下,`--color` 只会在计算值等于 `5` 的时候设定为 `blue`,其他值的时候就变为预设值。 244 | 245 | 直接在 `tg-filter` 中加入 `!` 符号这种设计,主要是考虑到这种需求应该只会在 `tg-filter` 中发生。如果又另外建立一个属性来设定模式,可能会变得不需要甚至容易误解。 246 | 247 | ## 继承 248 | 249 | 就像一些 CSS 属性一样,`tg-` 属性(除了 `tg-follow`,`tg-ref`)的值会继承自父级元素(如果当前元素没有设定的话)。如果不希望继承父级元素的值,并设定为默认值的话,只需增加没有值的 `tg-` 属性即可。例如: 250 | 251 | ```html 252 |
253 | 254 | 255 | 256 |
257 | ``` 258 | 259 | ## `tg-edge` 图解 260 | 261 | `cover`(默认值)和 `inset` 的区别如下图所示: 262 | 263 | ![](/readme-assets/tg-edge-explaination.png) 264 | 265 | 所以当 `tg-edge="inset"`,元素的高度必须大于浏览器视窗(`window.clientHeight`)的高度。 266 | 267 | ## JavaScript 事件 268 | 269 | 我们也可监听指定元素的 `tg` 事件来获取值: 270 | 271 | ```html 272 |

280 | Trigger JS 281 |

282 | 283 | 292 | 293 | 298 | ``` 299 | 300 | ## 自定义前缀 301 | 302 | 假如你担心 `tg-` 前缀可能不符合 HTML 5 的标准,可以通过在 `body` 标签中加入 `data-trigger-prefix` 去自定义它: 303 | 304 | ```html 305 | 306 |
Hello, World
307 | 308 | ``` 309 | 310 | 上述例子中将前缀自定义为 `data-tg`。`data-*` 是完全符合 HTML5 标准的属性,用于设置自定义的属性和设定值。 311 | 312 | ## 参与开发 313 | 314 | 欢迎 Fork 这个 Repo 进行开发,并提交 Pull Requests。在 [GitHub Issues](https://github.com/triggerjs/trigger/issues) 回报 Bug,在 [GitHub Discussions](https://github.com/triggerjs/trigger/discussions) 讨论功能/想法/问题。 315 | 316 | ## 授权协议 317 | 318 | Trigger.js 使用 [MIT 授权](LICENSE). 319 | -------------------------------------------------------------------------------- /README.zh-Hant.md: -------------------------------------------------------------------------------- 1 | # Trigger JS 2 | 3 | [![Build Status](https://github.com/triggerjs/trigger/actions/workflows/build.yml/badge.svg)](https://github.com/triggerjs/trigger/blob/main/.github/workflows/build.yml) 4 | [![npm version](https://img.shields.io/npm/v/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 5 | [![npm downloads](https://img.shields.io/npm/dm/@triggerjs/trigger.svg)](https://www.npmjs.com/package/@triggerjs/trigger) 6 | ![GitHub Stars](https://img.shields.io/github/stars/triggerjs/trigger) 7 | ![Github Forks](https://img.shields.io/github/forks/triggerjs/trigger) 8 | ![GitHub Open Issues](https://img.shields.io/github/issues/triggerjs/trigger) 9 | ![License](https://img.shields.io/github/license/triggerjs/trigger) 10 | 11 | *毋須編寫 JavaScript,建立頁面捲動動畫。* 12 | 13 | 有時候我們想因應頁面捲動的位置建立動畫,或更新 HTML 元素的 CSS 樣式。就像影片播放一樣,動畫在頁面向上捲動時前進,向下捲動時後退。 14 | 15 | 使用 Trigger JS,在頁面捲動時即可透過 CSS 變數取得製作動畫所需的值,而毋須編寫任何 JavaScript 代碼,相關設置則可透過 HTML 屬性實現。[點擊這裡](https://codepen.io/collection/eJmoMr)查看範例。 16 | 17 | 閱讀本文檔的其他語言版本:[English](README.md)、[繁體中文](README.zh-Hant.md)、[简体中文](README.zh-Hans.md)。 18 | 19 | ## 如何使用 20 | 21 | #### 方法一:使用 CDN 版本 22 | 23 | 1. 透過 script 標籤將 trigger.js 加載到網頁中: 24 | - UNPKG CDN: 25 | ```html 26 | 27 | ``` 28 | - jsDelivr CDN: 29 | ```html 30 | 31 | ``` 32 | 33 | 2. 為對應的 DOM 元素加上 `tg-name` 屬性,設定值等於接收數值的 CSS 變數名。 34 | 35 | ```html 36 |
Hello, World
37 | ``` 38 | 39 | 上述例子中,CSS 變數 `--scrolled` 會被加入到 `#greeting` 選擇器中: 40 | 41 | ```html 42 | 51 | ``` 52 | 53 | 5. 捲動頁面,測試效果。 54 | 55 | #### 方法二:從原始碼打包 56 | 57 | 1. 從以下任一途徑獲取原始碼: 58 | - GitHub 59 | ```bash 60 | git clone https://github.com/triggerjs/trigger.git 61 | ``` 62 | - NPM 63 | ```bash 64 | npm i @triggerjs/trigger 65 | ``` 66 | 2. 切換到目錄內,安裝依賴: 67 | ```bash 68 | npm install 69 | ``` 70 | 3. 本身已經有一個預打包的版本在 `dist/bundle.js`。運行一個本地網頁伺服器,查看範例歡迎頁 `index.html`: 71 | 1. 例如透過運行 `npx serve` 72 | 2. 在瀏覽器中打開 `http://localhost:5000` 73 | 3. 捲動頁面,測試效果。 74 | 4. 以下指令會打包一個新的版本到 `dist/bundle.js`: 75 | - 開發環境版本(持續監聽修改): 76 | ```bash 77 | npm run watch 78 | ``` 79 | - 開發環境版本: 80 | ```bash 81 | npm run build 82 | ``` 83 | - 生產環境版本: 84 | ```bash 85 | npm run prod 86 | ``` 87 | 88 | ## `tg-` 屬性列表 89 | 90 | | 屬性名稱 | 類型 | 預設值 | 簡介 | 91 | | ----------- | ---- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 92 | | `tg-name` | 必填 | - | 接收捲動值的 CSS 變數名稱,是否加上 `--` 前綴都可。 | 93 | | `tg-from` | 選填 | `0` | 起始值 | 94 | | `tg-to` | 選填 | `1` | 終點值 | 95 | | `tg-steps` | 選填 | `100` | 從 `tg-from` 至 `tg-to` 之間觸發多少次 | 96 | | `tg-step` | 選填 | `0` | 每次遞加的數值,如果此值不為 `0`,則會忽略 `tg-steps` 的設定。 | 97 | | `tg-map` | 選填 | (空白字串) | 將一個值映射至另一個值。格式:
- 一個值映射至另一個值:`value: newValue; value2: newValue2`
- 多個值映射至另一個值:`value,value2,value3: newValue`
- 一個範圍映射至另一個值:`value...value2: newValue` | 98 | | `tg-filter` | 選填 | (空白字串) | 只當捲動值在列表當中時才觸發,格式:`1,3,5,7,9`。預設情況下,過濾模式是 `retain`(保留值),在設定值末端加入 `!` 符號可以將模式切換為 `exact`(絕對)。關於兩個模式的分別,請參考後續的內容。 | 99 | | `tg-edge` | 選填 | cover | 計算捲動值的起始點與結束點。`cover` 代表畫面外至畫面外,即在元素從底部進入畫面時開始計算,從頂部完整離開畫面時結束;`inset` 代表當元素的頂部觸及頁面的頂部時開始計算,元素的底部觸及頁面的底部時結束。以下將有圖解說明。 | 100 | | `tg-follow` | 選填 | (空白字串) | 引用其他元素的計算值。`tg-follow` 的設定值等於目標元素的 `tg-ref` 設定值。**注意**:當設定了 `tg-follow`,同一元素下的 `tg-from`、`tg-to`、`tg-steps`、`tg-step` 以及 `tg-edge` 設定會被忽略。 | 101 | | `tg-ref` | 選填 | (空白字串) | 定義可以被其他元素透過 `tg-follow` 引用的名稱。 | 102 | | `tg-bezier` | 選填 | (空白字符串) | 貝茲曲線設定,設定值為 `ease`、`easeIn`、`easeOut`、`easeInOut` 或自定義的貝茲曲線數值,格式是:`p1x,p1y,p2x,p2y`。 | 103 | 104 | ## 映射 (Value Mapping) 105 | 106 | 數字並不適用於所有情況。例如,我們希望在捲動頁面的時候更改文字顏色,這個時候 `tg-map` 屬性就派上用場了。 107 | 108 | 以下例子示範如何根據下表的規則更新文字顏色: 109 | 110 | | 元素位置(從頁底起) | 捲動的值 | 文字顏色 | 111 | | -------------------- | -------- | -------- | 112 | | 0% - 10% | 1 | black | 113 | | 10% - 20% | 2 | red | 114 | | 20% - 30% | 3 | orange | 115 | | 30% - 40% | 4 | yellow | 116 | | 40% - 50% | 5 | green | 117 | | 50% - 60% | 6 | cyan | 118 | | 60% - 70% | 7 | blue | 119 | | 70% - 80% | 8 | purple | 120 | | 80% - 90% | 9 | grey | 121 | | 90% - 100% | 10 | grey | 122 | 123 | ```html 124 |

132 | 彩虹文字 133 |

134 | 135 | 144 | ``` 145 | 146 | ## Steps & Step 147 | 148 | 假設 `tg-from="200"` 以及 `tg-to="-200"`,我們想透過 `transform: translateX()` 將一個元素在 x 方向移動。`tg-steps` 讓我們設定從 `200` 到 `-200` 總共有多少步,舉個例子,`tg-steps="400"` 等於用 `400` 步從 `200` 遞減到 `-200`,每一步等於 `1`;換句話說,`tg-steps="800"` 就代表每一步等於 `0.5`。 149 | 150 | 但是有些時候,我們不想自行運算,這就是 `tg-step` 出現的原因。`tg-step` 直接定義每一步的值,所以如果定義了 `tg-step`,`tg-steps` 就會被忽略。 151 | 152 | ## 降噪 153 | 154 | 有時我們只對某些特定的值感興趣。例如,我們只想知道從 `0` 至 `100` (`tg-from="0"` 以及 `tg-to="100"`) 之間,什麼時候出現 `25, 50, 75`。在這個情況中,`tg-filter` 就可以幫上忙。 155 | 156 | ```html 157 |

166 | Red (25), Yellow (50), Green (75) 167 |

168 | 169 | 178 | ``` 179 | 180 | ## `tg-filter` 的模式 181 | 182 | `tg-filter` 有兩個模式,預設是 `retain`,另一個設定值是 `exact`。為了更好的說明差異,請參閱以下例子: 183 | 184 | ```html 185 |

194 | Trigger.js 195 |

196 | 197 | 207 | ``` 208 | 209 | 上述例子中,文字顏色初始是黑色,而將文字捲動到頁面中間時,會變為藍色。不過文字就永遠都不會變回黑色了,因為沒有讓它改變為黑色的觸發點。 210 | 211 | 如果我們想文字顏色只在計算值是 `5` 的時候改變為藍色,其他時候是黑色,可以將代碼更改為: 212 | 213 | ```html 214 |

223 | Trigger.js 224 |

225 | ``` 226 | 227 | 這樣雖然可以,不過很快我們的代碼就變得冗長。為了解決這個情況,我們可以將模式切換為 `exact`,只須在 `tg-filter` 的末端加入 `!` 符號即可: 228 | 229 | ```html 230 |

239 | Trigger.js 240 |

241 | ``` 242 | 243 | 在 `exact` 模式下,`--color` 只會在計算值等於 `5` 的時候設定為 `blue`,其他值的時候就變為預設值。 244 | 245 | 直接在 `tg-filter` 中加入 `!` 符號這種設計,主要是考慮到這種需求應該只會在 `tg-filter` 中發生。如果又另外建立一個屬性來設定模式,可能會變得不需要甚至容易誤解。 246 | 247 | ## 繼承 248 | 249 | 就像一些 CSS 屬性一樣,`tg-` 屬性(除了 `tg-follow`,`tg-ref`)的值會繼承自父級元素(如果當前元素沒有設定的話)。如果不希望繼承父級元素的值,並設定為預設值的話,只需增加沒有值的 `tg-` 屬性即可。例如: 250 | 251 | ```html 252 |
253 | 254 | 255 | 256 |
257 | ``` 258 | 259 | ## `tg-edge` 圖解 260 | 261 | `cover`(預設值)和 `edge` 的分別如下圖所示: 262 | 263 | ![](/readme-assets/tg-edge-explaination.png) 264 | 265 | 所以當 `tg-edge="inset"`,元素的高度必須大於瀏覽器視窗(`window.clientHeight`)的高度。 266 | 267 | ## JavaScript 事件 268 | 269 | 我們也可監聽指定元素的 `tg` 事件來獲取值: 270 | 271 | ```html 272 |

280 | Trigger JS 281 |

282 | 283 | 292 | 293 | 298 | ``` 299 | 300 | ## 自定義前綴 301 | 302 | 假如你擔心 `tg-` 前綴可能不符合 HTML 5 的標準,可以透過在 `body` 標籤中加入 `data-trigger-prefix` 去自定義它: 303 | 304 | ```html 305 | 306 |
Hello, World
307 | 308 | ``` 309 | 310 | 上述例子中將前綴自定義為 `data-tg`。`data-*` 是完全符合 HTML5 標準的屬性,用於設置自定義的屬性和設定值。 311 | 312 | ## 參與開發 313 | 314 | 歡迎 Fork 這個 Repo 進行開發,並提交 Pull Requests。在 [GitHub Issues](https://github.com/triggerjs/trigger/issues) 回報 Bug,在 [GitHub Discussions](https://github.com/triggerjs/trigger/discussions) 討論功能/想法/問題。 315 | 316 | ## 授權協議 317 | 318 | Trigger.js 使用 [MIT 授權](LICENSE). 319 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Trigger.js v1.0.8 3 | * Copyright (c) 2021 Steven Lei 4 | * Released under the MIT License. 5 | */(()=>{var t={635:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>o});var n=r(105);function o(t){if("string"==typeof t){if(t.indexOf(",")>-1){var e=t.split(",");if(4!==e.length)throw new Error("Bezier function expected 4 arguments, but got ".concat(e.length,"."));return e}if(!n.Si.hasOwnProperty(t))throw new Error("The default value of the bezier function does not exist!");return t}}},234:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>n});function n(t){return t&&["cover","inset"].includes(t)||(t="cover"),t}},921:(t,e,r)=>{"use strict";function n(t){var e={mode:"retain",values:[]};return"string"==typeof t&&""!==t.trim()&&("!"===t.substring(t.length-1)&&(e.mode="exact",t=t.substring(0,t.length-1)),t=t.replace(/!/g,""),e.values=t.split(",").map((function(t){return Number(t.trim())}))),e}r.r(e),r.d(e,{get:()=>n})},551:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>o});var n=r(381);function o(t){if(!t)return null;var e=document.querySelector("[".concat((0,n.G)(),'ref="').concat(t,'"]'));return!e||e.hasAttribute("".concat((0,n.G)(),"follow"))?null:e}},705:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>n});function n(t){return t?Number(t):0}},661:(t,e,r)=>{"use strict";function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==r)return;var n,o,i=[],u=!0,a=!1;try{for(r=r.call(t);!(u=(n=r.next()).done)&&(i.push(n.value),!e||i.length!==e);u=!0);}catch(t){a=!0,o=t}finally{try{u||null==r.return||r.return()}finally{if(a)throw o}}return i}(t,e)||function(t,e){if(!t)return;if("string"==typeof t)return o(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);"Object"===r&&t.constructor&&(r=t.constructor.name);if("Map"===r||"Set"===r)return Array.from(t);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return o(t,e)}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r-1)o[0].split(",").forEach((function(t){r[t.trim()]=o[1].trim()}));else if(o[0].indexOf("...")>-1)for(var i=n(o[0].split("...").map((function(t){return+t})).sort((function(t,e){return t-e})),2),u=i[0],a=i[1],c=u;c<=a;)c=Number(c.toFixed((null==e?void 0:e.decimals)||2)),r[c]=o[1].trim(),c+=(null==e?void 0:e.increment)||.01;else r[o[0].trim()]=o[1].trim()})),r}r.r(e),r.d(e,{get:()=>i})},424:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>o});var n=r(381);function o(t){return t||console.warn("".concat((0,n.G)(),"name is not set")),"--"===t.substring(0,2)?t:"--".concat(t)}},146:(t,e,r)=>{"use strict";function n(t){return t||null}r.r(e),r.d(e,{get:()=>n})},414:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>n});function n(t){return t?Number(t):0}},158:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>n});function n(t){var e=t?Number(t):100;return 0===e&&(e=100),e}},604:(t,e,r)=>{"use strict";r.r(e),r.d(e,{get:()=>n});function n(t){return t?Number(t):1}},105:(t,e,r)=>{"use strict";function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==r)return;var n,o,i=[],u=!0,a=!1;try{for(r=r.call(t);!(u=(n=r.next()).done)&&(i.push(n.value),!e||i.length!==e);u=!0);}catch(t){a=!0,o=t}finally{try{u||null==r.return||r.return()}finally{if(a)throw o}}return i}(t,e)||function(t,e){if(!t)return;if("string"==typeof t)return o(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);"Object"===r&&t.constructor&&(r=t.constructor.name);if("Map"===r||"Set"===r)return Array.from(t);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return o(t,e)}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);rd;){if(r=f(c)-t,Math.abs(r)0?s=c:d=c,c=(s+d)/2}return c}(t),((c*e+l)*e+s)*e;var e}}r.d(e,{Si:()=>u,$p:()=>a});i(.25,.1,.25,1),i(.42,0,1,1),i(0,0,.58,1),i(.42,0,.58,1);var u={ease:i(.25,.1,.25,1),easeIn:i(.42,0,1,1),easeOut:i(0,0,.58,1),easeInOut:i(.42,0,.58,1)};function a(t,e){if("string"==typeof t)e=u[t](e);else{var r=n(t,4);e=i(r[0],r[1],r[2],r[3])(e)}return e}},381:(t,e,r)=>{"use strict";r.d(e,{P:()=>o,G:()=>i});var n="tg";function o(){var t=document.body&&document.body.getAttribute("data-trigger-prefix");t&&function(t){if("string"!=typeof t||!(t=t.trim()))return;n=t}(t)}function i(){return"".concat(n,"-")}},523:(t,e,r)=>{var n={"./tg-bezier.ts":635,"./tg-edge.ts":234,"./tg-filter.ts":921,"./tg-follow.ts":551,"./tg-from.ts":705,"./tg-map.ts":661,"./tg-name.ts":424,"./tg-ref.ts":146,"./tg-step.ts":414,"./tg-steps.ts":158,"./tg-to.ts":604};function o(t){var e=i(t);return r(e)}function i(t){if(!r.o(n,t)){var e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}return n[t]}o.keys=function(){return Object.keys(n)},o.resolve=i,t.exports=o,o.id=523}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n](i,i.exports,r),i.exports}r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var n={};(()=>{"use strict";r.r(n);var t=r(381);var e=["tg-follow","tg-ref"],o={},i=r(523);function u(r,n,i){var u=n;if("tg-"!==u.substring(0,3)){var a=(0,t.G)();u="tg-".concat(u.replace(a,""))}if(void 0===o[u])return null;var c=function(t,r,n){if(t.hasAttribute(r)||e.includes(n))return t;var o=t;for(;(o=o.parentElement)!==document.body;)if(o.hasAttribute(r))return o;return t}(r,n,u),l=c.hasAttribute(n)?c.getAttribute(n):null;return o[u].get(l,i)}i.keys().map((function(t){var e,r=null===(e=t.match(/tg-[^.]+/))||void 0===e?void 0:e[0];r&&(o[r]=i(t))}));var a=r(105);function c(e){var r,n=(0,t.G)(),o=u(e,"".concat(n,"follow"))||e,i=getComputedStyle(o),a=+i.getPropertyValue("--".concat(n,"top")),c=+i.getPropertyValue("--".concat(n,"height")),l=u(e,"".concat(n,"name")),s=u(o,"".concat(n,"from")),f=u(o,"".concat(n,"to")),d=u(o,"".concat(n,"steps")),m=u(o,"".concat(n,"step")),g=u(o,"".concat(n,"bezier")),p=u(e,"".concat(n,"filter")),v=u(o,"".concat(n,"edge")),y=Math.abs(f-s),b=0===m?y/d:m,h=y/b,w=(r=b,Math.floor(r)===r?0:r.toString().split(".")[1].length||0),A=s>f?-1:1;return{el:e,top:a,height:c,name:l,from:s,to:f,steps:d,step:m,mapping:u(e,"".concat(n,"map"),{increment:b,decimals:w}),filter:p,edge:v,range:y,increment:b,segments:h,decimals:w,multiplier:A,lastValue:null,bezier:g}}function l(t){var e=document.documentElement,r=e.scrollTop,n=e.clientHeight;t.forEach((function(t){var e=t.el,o=t.top,i=t.height,u=t.increment,c=t.segments,l=t.decimals,s=t.multiplier,f=t.name,d=t.from,m=t.mapping,g=t.filter,p=t.edge,v=t.lastValue,y=t.bezier;if("--_"!==f){var b,h="cover"===p?Math.min(Math.max((r+n-o)/(n+i),0),1):Math.min(Math.max((r-o)/(i-n),0),1);h=y?(0,a.$p)(y,h):h,b=+(d+Math.floor((c+1)*h)*u*s).toFixed(l),g.values.length>0&&!g.values.includes(b)?"exact"===g.mode&&(t.lastValue=null,e.style.removeProperty(f)):(void 0!==m[b]&&(b=m[b]),v!=b&&(e.style.setProperty(f,"".concat(b)),e.dispatchEvent(new CustomEvent("tg",{target:e,detail:{value:b}})),t.lastValue=b))}}))}var s={root:null,rootMargin:"0px",threshold:0};function f(e,r){r&&"function"==typeof r.before&&r.before();var n=(0,t.G)();document.querySelectorAll("[".concat(n,"name]")).forEach((function(t){var r=t.getBoundingClientRect(),o=r.top,i=r.height;t.style.setProperty("--".concat(n,"top"),"".concat(o+window.scrollY)),t.style.setProperty("--".concat(n,"height"),"".concat(i)),e&&e.observe(t)})),r&&"function"==typeof r.after&&r.after()}function d(t){return function(t){if(Array.isArray(t))return m(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(!t)return;if("string"==typeof t)return m(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);"Object"===r&&t.constructor&&(r=t.constructor.name);if("Map"===r||"Set"===r)return Array.from(t);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return m(t,e)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function m(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r 2 | 3 | 4 | 5 | 6 | 7 | Hello. 8 | 9 | 10 | 11 | 45 | 46 | 47 |
55 |
56 |
57 | Hello. 58 |
59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@triggerjs/trigger", 3 | "version": "1.0.8", 4 | "description": "A library for creating scroll-based animation with HTML attributes and CSS variables.", 5 | "main": "index.ts", 6 | "scripts": { 7 | "build": "webpack --mode development", 8 | "prod": "webpack --mode production", 9 | "watch": "webpack --watch --mode development", 10 | "format": "prettier --write '**/*.{ts,tsx,js,jsx,json,md}'", 11 | "prepare": "husky install" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/triggerjs/trigger.git" 16 | }, 17 | "author": "Steven Lei", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/triggerjs/trigger/issues" 21 | }, 22 | "homepage": "https://github.com/triggerjs/trigger#readme", 23 | "devDependencies": { 24 | "@babel/core": "^7.15.8", 25 | "@babel/preset-env": "^7.15.8", 26 | "@babel/preset-typescript": "^7.15.0", 27 | "@types/webpack-env": "^1.16.3", 28 | "babel-loader": "^8.2.2", 29 | "fork-ts-checker-webpack-plugin": "^6.4.0", 30 | "husky": "^7.0.0", 31 | "lint-staged": "^11.2.5", 32 | "prettier": "^2.4.1", 33 | "typescript": "^4.4.4", 34 | "webpack": "^5.59.0", 35 | "webpack-cli": "^4.9.1" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "unpkg": "dist/bundle.js", 41 | "jsdelivr": "dist/bundle.js", 42 | "lint-staged": { 43 | "src/**/*.{ts,tsx,js,jsx,json,md}": [ 44 | "prettier --write" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | arrowParens: 'always', 7 | }; 8 | -------------------------------------------------------------------------------- /readme-assets/tg-edge-explaination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triggerjs/trigger/3e85732cea57bae044bbaaee2d33c506d55f88e4/readme-assets/tg-edge-explaination.png -------------------------------------------------------------------------------- /src/bind.ts: -------------------------------------------------------------------------------- 1 | import { getPrefix } from './prefix'; 2 | 3 | export interface BindHook { 4 | before?: () => void; 5 | after?: () => void; 6 | } 7 | 8 | export default function ( 9 | observer: IntersectionObserver | null, 10 | hook?: BindHook 11 | ) { 12 | // Before Hook 13 | hook && typeof hook.before === 'function' && hook.before(); 14 | 15 | // Fetch all DOM elements with [tg-name] attribute and set the current top & left offset 16 | const prefix = getPrefix() 17 | document 18 | .querySelectorAll(`[${prefix}name]`) 19 | .forEach((element) => { 20 | const { top, height } = element.getBoundingClientRect(); 21 | 22 | element.style.setProperty(`--${prefix}top`, `${top + window.scrollY}`); 23 | element.style.setProperty(`--${prefix}height`, `${height}`); 24 | 25 | observer && observer.observe(element); 26 | }); 27 | 28 | // After Hook 29 | hook && typeof hook.after === 'function' && hook.after(); 30 | } 31 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { getPrefix } from '../prefix'; 2 | import { CustomDirective, TgDirective, TgElementExtraData } from '../type'; 3 | 4 | const SHOULD_NOT_INHERIT_DIRECTIVES = [`tg-follow`, `tg-ref`]; 5 | 6 | // As we cannot get the latest value of getPrefix here 7 | // (require('...') run at webpack build time) 8 | // so we have to use tg- here as key first 9 | // and deal with custom prefix later 10 | 11 | // Declare Object directives 12 | const directives: Record = {}; 13 | 14 | // Load all the directives 15 | const importDir = require.context('./', false, /tg-[\S]+\.ts$/); 16 | importDir.keys().map((key) => { 17 | const formatKey = key.match(/tg-[^.]+/)?.[0] as TgDirective | null; 18 | if (formatKey) { 19 | directives[formatKey] = importDir(key); 20 | } 21 | }); 22 | 23 | // Extract the value of tg element 24 | export function extractValues( 25 | element: HTMLElement, 26 | directive: CustomDirective, 27 | data?: TgElementExtraData 28 | ) { 29 | // Check if the directive prefix is customised 30 | // Replace custom prefix to tg- here if necessary 31 | // in order to have the correct object key 32 | // for getting the correct value from directives object 33 | let directiveKey = directive as TgDirective; 34 | 35 | if (directiveKey.substring(0, 3) !== 'tg-') { 36 | const newPrefix = getPrefix(); 37 | directiveKey = `tg-${directiveKey.replace(newPrefix, '')}`; 38 | } 39 | 40 | if (typeof directives[directiveKey] === 'undefined') { 41 | return null; 42 | } 43 | 44 | const targetElement = selfOrInheritFromParent( 45 | element, 46 | directive, 47 | directiveKey 48 | ); 49 | 50 | // In order to know whether the attribute present or not 51 | const value = targetElement.hasAttribute(directive) 52 | ? targetElement.getAttribute(directive) 53 | : null; 54 | 55 | return directives[directiveKey].get(value, data); 56 | } 57 | 58 | // Find the target element to get the value, is it from self, or inherit from parents? 59 | function selfOrInheritFromParent( 60 | el: HTMLElement, 61 | directive: CustomDirective, 62 | directiveKey: TgDirective 63 | ) { 64 | // If the current element has already been set the directive 65 | if ( 66 | el.hasAttribute(directive) || 67 | SHOULD_NOT_INHERIT_DIRECTIVES.includes(directiveKey) 68 | ) { 69 | return el; 70 | } 71 | 72 | let currentEl: HTMLElement = el; 73 | 74 | // Traverse parents 75 | while (true) { 76 | currentEl = currentEl.parentElement!; 77 | 78 | // Already arrives to body, stop 79 | if (currentEl === document.body) { 80 | break; 81 | } 82 | 83 | if (currentEl.hasAttribute(directive)) { 84 | return currentEl; 85 | } 86 | } 87 | 88 | return el; // Return original (self) element, as no results from parents 89 | } 90 | -------------------------------------------------------------------------------- /src/directives/tg-bezier.ts: -------------------------------------------------------------------------------- 1 | import { defaultBezier } from '../ease'; 2 | 3 | export function get(value?: string) { 4 | if (typeof value === 'string') { 5 | if (value.indexOf(',') > -1) { 6 | const arr = value.split(','); 7 | if (arr.length !== 4) { 8 | throw new Error( 9 | `Bezier function expected 4 arguments, but got ${arr.length}.` 10 | ); 11 | } 12 | return arr; 13 | } else if (!defaultBezier.hasOwnProperty(value)) { 14 | // Available named bezier values: `ease`, `easeIn`, `easeOut`, `easeInOut` 15 | throw new Error( 16 | 'The default value of the bezier function does not exist!' 17 | ); 18 | } 19 | return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/directives/tg-edge.ts: -------------------------------------------------------------------------------- 1 | export type EdgeOptions = 'cover' | 'inset'; 2 | // Default Value 3 | const DEFAULT: EdgeOptions = 'cover'; 4 | 5 | export function get(value?: EdgeOptions) { 6 | // only supports cover / inset for now 7 | if (!value || !['cover', 'inset'].includes(value)) { 8 | value = DEFAULT; 9 | } 10 | 11 | return value; 12 | } 13 | -------------------------------------------------------------------------------- /src/directives/tg-filter.ts: -------------------------------------------------------------------------------- 1 | export interface FilterValue { 2 | mode: 'retain' | 'exact'; 3 | values: number[]; 4 | } 5 | 6 | export function get(value?: string) { 7 | const filter: FilterValue = { 8 | mode: 'retain', // mode = retain / exact 9 | values: [], 10 | }; 11 | 12 | if (typeof value === 'string' && value.trim() !== '') { 13 | // switch mode to 'exact', means that if the value is other 14 | // than this, should remove the css attribute (in order to keep the 15 | // default value of the css variable) 16 | if (value.substring(value.length - 1) === '!') { 17 | filter.mode = 'exact'; 18 | value = value.substring(0, value.length - 1); 19 | } 20 | 21 | // Clean up all exclamation marks if added accidentally 22 | value = value.replace(/!/g, ''); 23 | 24 | filter.values = value.split(',').map((item) => Number(item.trim())); 25 | } 26 | 27 | return filter; 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/tg-follow.ts: -------------------------------------------------------------------------------- 1 | import { getPrefix } from '../prefix'; 2 | 3 | export function get(value?: string) { 4 | if (!value) { 5 | return null; 6 | } 7 | 8 | // Suppose tg-ref is unique like id attribute 9 | const follow = document.querySelector(`[${getPrefix()}ref="${value}"]`); 10 | 11 | // Do not support follow chain right now 12 | if (!follow || follow.hasAttribute(`${getPrefix()}follow`)) { 13 | return null; 14 | } 15 | 16 | return follow as HTMLElement; 17 | } 18 | -------------------------------------------------------------------------------- /src/directives/tg-from.ts: -------------------------------------------------------------------------------- 1 | import { AttributeNumber } from '../type'; 2 | 3 | // Default value 4 | const DEFAULT = 0; 5 | 6 | export function get(value?: AttributeNumber) { 7 | return value ? Number(value) : DEFAULT; 8 | } 9 | -------------------------------------------------------------------------------- /src/directives/tg-map.ts: -------------------------------------------------------------------------------- 1 | import { TgElementExtraData } from '../type'; 2 | 3 | export function get(value?: string, data?: TgElementExtraData) { 4 | const items: Record = {}; 5 | 6 | if (typeof value === 'string' && value.trim() !== '') { 7 | value.split(';').forEach((pair) => { 8 | let arr = pair.split(':'); 9 | 10 | if (arr.length === 2) { 11 | if (arr[0].indexOf(',') > -1) { 12 | // Multiple Values 13 | arr[0].split(',').forEach((key) => { 14 | items[key.trim()] = arr[1].trim(); 15 | }); 16 | } else if (arr[0].indexOf('...') > -1) { 17 | // Use `...` here to define a range, inspired by Swift range operator. 18 | // Instead of using `-`, we can handle negative numbers easily. 19 | let [from, to] = arr[0] 20 | .split('...') 21 | .map((val) => { 22 | return +val; 23 | }) 24 | .sort((a, b) => a - b); 25 | 26 | let i = from; 27 | while (i <= to) { 28 | i = Number(i.toFixed(data?.decimals || 2)); 29 | items[i] = arr[1].trim(); 30 | 31 | i += data?.increment || 0.01; 32 | } 33 | } else { 34 | items[arr[0].trim()] = arr[1].trim(); 35 | } 36 | } 37 | }); 38 | } 39 | return items; 40 | } 41 | -------------------------------------------------------------------------------- /src/directives/tg-name.ts: -------------------------------------------------------------------------------- 1 | import { getPrefix } from '../prefix'; 2 | import { CssVariable } from '../type'; 3 | 4 | export function get(value: string): CssVariable { 5 | if (!value) { 6 | console.warn(`${getPrefix()}name is not set`); 7 | } 8 | 9 | if (value.substring(0, 2) === `--`) { 10 | return value as CssVariable; 11 | } 12 | 13 | // Auto prepend -- for a CSS variable name 14 | return `--${value}`; 15 | } 16 | -------------------------------------------------------------------------------- /src/directives/tg-ref.ts: -------------------------------------------------------------------------------- 1 | export function get(value?: string) { 2 | if (!value) { 3 | return null; 4 | } 5 | 6 | return value; 7 | } 8 | -------------------------------------------------------------------------------- /src/directives/tg-step.ts: -------------------------------------------------------------------------------- 1 | import { AttributeNumber } from '../type'; 2 | 3 | // Default value 4 | const DEFAULT = 0; 5 | 6 | export function get(value?: AttributeNumber) { 7 | return value ? Number(value) : DEFAULT; 8 | } 9 | -------------------------------------------------------------------------------- /src/directives/tg-steps.ts: -------------------------------------------------------------------------------- 1 | import { AttributeNumber } from '../type'; 2 | 3 | // Default value 4 | const DEFAULT = 100; 5 | 6 | export function get(value?: AttributeNumber) { 7 | let result = value ? Number(value) : DEFAULT; 8 | 9 | // Should never be 0 10 | if (result === 0) { 11 | result = DEFAULT; 12 | } 13 | 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /src/directives/tg-to.ts: -------------------------------------------------------------------------------- 1 | import { AttributeNumber } from '../type'; 2 | 3 | // Default value 4 | const DEFAULT = 1; 5 | 6 | export function get(value?: AttributeNumber) { 7 | return value ? Number(value) : DEFAULT; 8 | } 9 | -------------------------------------------------------------------------------- /src/ease.ts: -------------------------------------------------------------------------------- 1 | import { BezierOption } from './type'; 2 | 3 | export function cubicBezier( 4 | p1x: number, 5 | p1y: number, 6 | p2x: number, 7 | p2y: number 8 | ) { 9 | const ZERO_LIMIT = 1e-6; 10 | // Calculate the polynomial coefficients, 11 | // implicit first and last control points are (0,0) and (1,1). 12 | const ax = 3 * p1x - 3 * p2x + 1; 13 | const bx = 3 * p2x - 6 * p1x; 14 | const cx = 3 * p1x; 15 | 16 | const ay = 3 * p1y - 3 * p2y + 1; 17 | const by = 3 * p2y - 6 * p1y; 18 | const cy = 3 * p1y; 19 | 20 | function sampleCurveDerivativeX(t: number) { 21 | // `ax t^3 + bx t^2 + cx t` expanded using Horner's rule 22 | return (3 * ax * t + 2 * bx) * t + cx; 23 | } 24 | 25 | function sampleCurveX(t: number) { 26 | return ((ax * t + bx) * t + cx) * t; 27 | } 28 | 29 | function sampleCurveY(t: number) { 30 | return ((ay * t + by) * t + cy) * t; 31 | } 32 | 33 | // Given an x value, find a parametric value it came from. 34 | function solveCurveX(x: number) { 35 | let t2 = x; 36 | let derivative; 37 | let x2; 38 | 39 | // https://trac.webkit.org/browser/trunk/Source/WebCore/platform/animation 40 | // first try a few iterations of Newton's method -- normally very fast. 41 | // http://en.wikipedia.org/wikiNewton's_method 42 | for (let i = 0; i < 8; i++) { 43 | // f(t) - x = 0 44 | x2 = sampleCurveX(t2) - x; 45 | if (Math.abs(x2) < ZERO_LIMIT) { 46 | return t2; 47 | } 48 | derivative = sampleCurveDerivativeX(t2); 49 | // == 0, failure 50 | /* istanbul ignore if */ 51 | if (Math.abs(derivative) < ZERO_LIMIT) { 52 | break; 53 | } 54 | t2 -= x2 / derivative; 55 | } 56 | 57 | // Fall back to the bisection method for reliability. 58 | // bisection 59 | // http://en.wikipedia.org/wiki/Bisection_method 60 | let t1 = 1; 61 | /* istanbul ignore next */ 62 | let t0 = 0; 63 | 64 | /* istanbul ignore next */ 65 | t2 = x; 66 | /* istanbul ignore next */ 67 | while (t1 > t0) { 68 | x2 = sampleCurveX(t2) - x; 69 | if (Math.abs(x2) < ZERO_LIMIT) { 70 | return t2; 71 | } 72 | if (x2 > 0) { 73 | t1 = t2; 74 | } else { 75 | t0 = t2; 76 | } 77 | t2 = (t1 + t0) / 2; 78 | } 79 | 80 | // Failure 81 | return t2; 82 | } 83 | 84 | function solve(x: number) { 85 | return sampleCurveY(solveCurveX(x)); 86 | } 87 | 88 | return solve; 89 | } 90 | 91 | export const ease = cubicBezier(0.25, 0.1, 0.25, 1); 92 | 93 | export const easeIn = cubicBezier(0.42, 0, 1, 1); 94 | 95 | export const easeOut = cubicBezier(0, 0, 0.58, 1); 96 | 97 | export const easeInOut = cubicBezier(0.42, 0, 0.58, 1); 98 | 99 | // Default named bezier values 100 | export const defaultBezier = { 101 | ease: cubicBezier(0.25, 0.1, 0.25, 1), 102 | easeIn: cubicBezier(0.42, 0, 1, 1), 103 | easeOut: cubicBezier(0, 0, 0.58, 1), 104 | easeInOut: cubicBezier(0.42, 0, 0.58, 1), 105 | }; 106 | 107 | export function easePercentage(bezier: BezierOption, percentage: number): number { 108 | if (typeof bezier === 'string') { 109 | percentage = defaultBezier[bezier](percentage); 110 | } else { 111 | const [p1x, p1y, p2x, p2y] = bezier; 112 | percentage = cubicBezier(p1x, p1y, p2x, p2y)(percentage); 113 | } 114 | return percentage; 115 | }; -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate the length of decimal places for a number 3 | * @param {number} number The number. 4 | * @returns {number} The caculated length. 5 | */ 6 | export function decimalsLength(number: number): number { 7 | if (Math.floor(number) === number) return 0; 8 | return number.toString().split('.')[1].length || 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Trigger from './trigger'; 2 | 3 | // Simple, Start. 4 | Trigger.start(); 5 | -------------------------------------------------------------------------------- /src/observer.ts: -------------------------------------------------------------------------------- 1 | const options: IntersectionObserverInit = { 2 | root: null, // document.body 3 | rootMargin: '0px', 4 | threshold: 0, 5 | }; 6 | 7 | export default function (cb: IntersectionObserverCallback) { 8 | // Check if `IntersectionObserver` is available 9 | if (typeof IntersectionObserver === 'undefined') { 10 | console.warn(`IntersectionObserver is not supported in this browser`); 11 | 12 | return null; 13 | } 14 | 15 | return new IntersectionObserver(cb, options); 16 | } 17 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import { decimalsLength } from './helpers'; 2 | import { extractValues } from './directives'; 3 | import { getPrefix } from './prefix'; 4 | import { FilterValue } from './directives/tg-filter'; 5 | import { EdgeOptions } from './directives/tg-edge'; 6 | import { TgElement } from './type'; 7 | import { easePercentage as ease } from './ease'; 8 | 9 | /** 10 | * This function will be called in observe stage, 11 | * caching those values into an object for ease of use in scroll event. 12 | */ 13 | export function parseAttributes(element: HTMLElement): TgElement { 14 | const prefix = getPrefix() 15 | const follow: HTMLElement = extractValues(element, `${prefix}follow`); 16 | 17 | const actualElement = follow || element; 18 | 19 | const style = getComputedStyle(actualElement); 20 | const top = +style.getPropertyValue(`--${prefix}top`); 21 | const height = +style.getPropertyValue(`--${prefix}height`); 22 | 23 | const name: string = extractValues(element, `${prefix}name`); 24 | const from: number = extractValues(actualElement, `${prefix}from`); 25 | const to: number = extractValues(actualElement, `${prefix}to`); 26 | const steps: number = extractValues(actualElement, `${prefix}steps`); 27 | const step: number = extractValues(actualElement, `${prefix}step`); 28 | const bezier: string | Array = extractValues( 29 | actualElement, 30 | `${prefix}bezier` 31 | ); 32 | 33 | const filter: FilterValue = extractValues(element, `${prefix}filter`); 34 | const edge: EdgeOptions = extractValues(actualElement, `${prefix}edge`); 35 | 36 | const range = Math.abs(to - from); 37 | const increment = step === 0 ? range / steps : step; 38 | const segments = range / increment; 39 | const decimals = decimalsLength(increment); 40 | const multiplier = from > to ? -1 : 1; 41 | 42 | const mapping: Record = extractValues( 43 | element, 44 | `${prefix}map`, 45 | { 46 | increment, 47 | decimals, 48 | } 49 | ); 50 | 51 | return { 52 | el: element, 53 | top, 54 | height, 55 | name, 56 | from, 57 | to, 58 | steps, 59 | step, 60 | mapping, 61 | filter, 62 | edge, 63 | range, 64 | increment, 65 | segments, 66 | decimals, 67 | multiplier, 68 | lastValue: null, 69 | bezier, 70 | }; 71 | } 72 | 73 | /** 74 | * Calculation happens here, 75 | * this function is called when scroll event happens. 76 | * So keep this as light as possible. 77 | */ 78 | export function parseValues(elements: TgElement[]) { 79 | const { scrollTop: scrolled, clientHeight } = document.documentElement; 80 | 81 | elements.forEach((element) => { 82 | const { 83 | el, 84 | top, 85 | height, 86 | increment, 87 | segments, 88 | decimals, 89 | multiplier, 90 | name, 91 | from, 92 | // currently unused 93 | // to, 94 | mapping, 95 | filter, 96 | edge, 97 | lastValue, 98 | bezier, 99 | } = element; 100 | 101 | // If the name is equal to '_' (--_), skip 102 | if (name === '--_') { 103 | return; 104 | } 105 | 106 | // edge is 'cover' by default 107 | let percentage = 108 | edge === 'cover' 109 | ? Math.min( 110 | Math.max( 111 | (scrolled + clientHeight - top) / (clientHeight + height), 112 | 0 113 | ), 114 | 1 115 | ) 116 | : Math.min(Math.max((scrolled - top) / (height - clientHeight), 0), 1); 117 | 118 | 119 | 120 | // Calculation result value of bezier 121 | percentage = bezier ? ease(bezier, percentage) : percentage; 122 | 123 | let value: string | number; 124 | 125 | const mappingValue = ( 126 | from + 127 | Math.floor((segments + 1) * percentage) * increment * multiplier 128 | ).toFixed(decimals); 129 | value = +mappingValue; 130 | 131 | if (filter.values.length > 0 && !filter.values.includes(value)) { 132 | // If the mode is 'exact', remove the CSS property 133 | // Setting the lastValue to null to ensure correct comparison below 134 | if (filter.mode === 'exact') { 135 | element.lastValue = null; 136 | el.style.removeProperty(name); 137 | } 138 | return; 139 | } 140 | 141 | if (typeof mapping[value] !== 'undefined') { 142 | value = mapping[value]; 143 | } 144 | 145 | if (lastValue != value) { 146 | el.style.setProperty(name, `${value}`); 147 | el.dispatchEvent( 148 | new CustomEvent('tg', { 149 | // @ts-ignore 150 | target: el, 151 | detail: { 152 | value, 153 | }, 154 | }) 155 | ); 156 | 157 | element.lastValue = value; 158 | console.log('value', element, value); 159 | } 160 | }); 161 | } -------------------------------------------------------------------------------- /src/prefix.ts: -------------------------------------------------------------------------------- 1 | import { Prefix } from './type'; 2 | 3 | let prefix = `tg`; 4 | 5 | export function getPrefixSetting() { 6 | const newPrefix = document.body && document.body.getAttribute('data-trigger-prefix'); 7 | newPrefix && setPrefix(newPrefix); 8 | } 9 | 10 | function setPrefix(str: string) { 11 | if (typeof str !== 'string' || !(str = str.trim())) { 12 | return; 13 | } 14 | 15 | prefix = str; 16 | } 17 | 18 | export function getPrefix(): Prefix { 19 | return `${prefix}-`; 20 | } 21 | -------------------------------------------------------------------------------- /src/trigger.ts: -------------------------------------------------------------------------------- 1 | import { getPrefixSetting, getPrefix } from './prefix'; 2 | import { parseAttributes, parseValues } from './parse'; 3 | import observer from './observer'; 4 | import bind from './bind'; 5 | 6 | import { TgElement, Trigger as TriggerType } from './type'; 7 | 8 | let activeElements: TgElement[] = []; // Store the elements observed by IntersectionObserver 9 | let ob: IntersectionObserver | null = null; // Store the observer instance 10 | 11 | getPrefixSetting(); // Get the customised prefix setting if available 12 | 13 | /** 14 | * Observe all `HTMLElement`. 15 | * 16 | * @private 17 | */ 18 | function observeElements() { 19 | ob = observer((entries) => { 20 | entries.forEach((entry) => { 21 | let { target } = entry; 22 | if (entry.isIntersecting) { 23 | activeElements.push(parseAttributes(target as HTMLElement)); 24 | } else { 25 | // Remove element from array if not intersecting 26 | activeElements = activeElements.filter(function (obj) { 27 | return obj.el !== target; 28 | }); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | /** 35 | * Add event listener for `DOMContentLoaded`, `resize`, `scroll` events of window. 36 | * 37 | * @private 38 | */ 39 | function eventListeners() { 40 | // Bind tg elements 41 | window.addEventListener('DOMContentLoaded', () => { 42 | // Find all [tg-name] elements 43 | bind(ob); 44 | 45 | setTimeout(() => { 46 | // Run once on start, so that correct style will be set before scroll happens 47 | let allElements = [ 48 | ...document.querySelectorAll(`[${getPrefix()}name]`), 49 | ].map((element) => { 50 | return parseAttributes(element as HTMLElement); 51 | }); 52 | parseValues(allElements); 53 | }); 54 | }); 55 | 56 | // Re-bind if resize occurs 57 | window.addEventListener('resize', () => { 58 | bind(ob, { 59 | before: () => { 60 | // Clean Up if necessary 61 | activeElements.forEach((element) => { 62 | ob?.unobserve(element.el); 63 | }); 64 | // Clean up activeElements 65 | activeElements = []; 66 | }, 67 | }); 68 | }); 69 | 70 | // Update the value of CSS variable for [tg-name] elements when scroll event happens 71 | window.addEventListener('scroll', (e) => { 72 | parseValues(activeElements); 73 | }); 74 | } 75 | 76 | const Trigger: TriggerType = { 77 | start() { 78 | if (!document.body) { 79 | console.warn(`Unable to initialise, document.body does not exist.`); 80 | return; 81 | } 82 | 83 | observeElements(); 84 | eventListeners(); 85 | }, 86 | }; 87 | 88 | export default Trigger; 89 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import { EdgeOptions } from './directives/tg-edge'; 2 | import { FilterValue } from './directives/tg-filter'; 3 | export type BezierOption = string | Array 4 | export type CssVariable = `--${string}`; 5 | export type AttributeNumber = `${number}` | number; 6 | export type Prefix = `${string}-`; 7 | export type CustomDirective = `${T}-${string}`; 8 | export type TgDirective = CustomDirective<'tg'>; 9 | 10 | export interface TgElement { 11 | el: HTMLElement; 12 | top: number; 13 | height: number; 14 | name: string; 15 | from: number; 16 | to: number; 17 | steps: number; 18 | step: number; 19 | mapping: Record; 20 | filter: FilterValue; 21 | edge: EdgeOptions; 22 | range: number; 23 | increment: number; 24 | segments: number; 25 | decimals: number; 26 | multiplier: number; 27 | lastValue: string | number | null; 28 | bezier: string | Array 29 | } 30 | 31 | export interface Trigger { 32 | start: () => void; 33 | } 34 | 35 | export interface TgElementExtraData { 36 | increment?: number; 37 | decimals?: number; 38 | } 39 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | # 3 | # DO NOT REMOVE OR EDIT THIS WARNING: 4 | # 5 | # This file is auto-generated by the TEA app. It is intended to validate ownership of your repository. 6 | # DO NOT commit this file or accept any PR if you don't know what this is. 7 | # We are aware that spammers will try to use this file to try to profit off others' work. 8 | # We take this very seriously and will take action against any malicious actors. 9 | # 10 | # If you are not the owner of this repository, and someone maliciously opens a commit with this file 11 | # please report it to us at support@tea.xyz. 12 | # 13 | # A constitution without this header is invalid. 14 | --- 15 | version: 2.0.0 16 | codeOwners: 17 | - '0xb9eB4f7504A30c0450CdeE926b341a065Ca1b43B' 18 | quorum: 1 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "suppressImplicitAnyIndexErrors": true, 10 | "strict": true, 11 | "downlevelIteration": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "CommonJS", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx" 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 5 | const { version } = require('./package.json'); 6 | 7 | /** 8 | * @type {webpack.Configuration} 9 | */ 10 | module.exports = { 11 | entry: './src/index.ts', 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | library: { 16 | name: 'Trigger', 17 | type: 'window', 18 | }, 19 | }, 20 | resolve: { 21 | extensions: ['.ts', '.js', '.json'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(t|j)s$/, 27 | loader: 'babel-loader', 28 | exclude: /(node_modules)/, 29 | }, 30 | ], 31 | }, 32 | optimization: { 33 | minimizer: [ 34 | new TerserPlugin({ 35 | terserOptions: { 36 | compress: { 37 | pure_funcs: ['console.info', 'console.debug', 'console.log'], 38 | }, 39 | }, 40 | extractComments: false, 41 | }), 42 | ], 43 | }, 44 | plugins: [ 45 | new ForkTsCheckerWebpackPlugin(), 46 | new webpack.BannerPlugin({ 47 | banner: `/*!\n * Trigger.js v${version}\n * Copyright (c) 2021 Steven Lei\n * Released under the MIT License.\n*/`, 48 | raw: true, 49 | }), 50 | ], 51 | }; 52 | --------------------------------------------------------------------------------