Merge pull request #22 from pdsuwwz/feature/vue3-support

🌈 feat: upgrade to Vue 3
This commit is contained in:
Wisdom
2022-01-01 16:22:30 +08:00
committed by GitHub
18 changed files with 5969 additions and 8661 deletions

118
README.md
View File

@@ -2,34 +2,49 @@
![npm](https://img.shields.io/npm/v/@hoc-element/affix) ![NPM](https://img.shields.io/npm/l/@hoc-element/affix) ![npm](https://img.shields.io/npm/v/@hoc-element/affix) ![NPM](https://img.shields.io/npm/l/@hoc-element/affix)
📌 将页面元素固定在可视范围内。 📌 基于 Webpack 5 构建的 Vue 3.x 固钉组件,用于将页面元素固定在可视范围内。
**Live demo:** https://pdsuwwz.github.io/hoc-element-affix **Live demo:** https://pdsuwwz.github.io/hoc-element-affix
## Version
* Vue 3.x 版本 [Vue 2.x 版本](https://github.com/pdsuwwz/hoc-element-affix/tree/vue2.0)
## Environment Support ## Environment Support
* Vue 2.6.0+ * Vue 3.2.x
## Install ## Install
```shell ```shell
npm install @hoc-element/affix npm install @hoc-element/affix
# or
pnpm add @hoc-element/affix
``` ```
## Quick Start ## Quick Start
```js ```js
import Vue from 'vue' import { createApp } from 'vue'
import HocElementAffix from '@hoc-element/affix' import HocElementAffix from '@hoc-element/affix'
import App from './App.vue'
Vue.use(HocElementAffix) createApp(App)
.use(HocElementAffix)
.mount('#app')
``` ```
## Feature ## Feature
- [x] 自定义顶部偏移量 - [x] 支持 Vue 3.x
- [x] 固定状态改变时触发的回调 - [x] 支持自定义顶部 `offsetTop` 偏移量
- [x] 插槽式的固定状态反馈 - [x] 支持固定状态改变触发回调
- [x] 支持 `Slot` 插槽式的固定状态反馈
## Using ## Using
@@ -56,92 +71,5 @@ Vue.use(HocElementAffix)
## Demo ## Demo
下面是比较全的例子,几乎囊括了 API 的所有用法,源码戳这: [Code](https://github.com/pdsuwwz/hoc-element-affix/tree/master/example/src/views/ExampleAffix.vue ) 下面是比较全的例子,几乎囊括了 API 的所有用法,源码戳这: [Code](https://github.com/pdsuwwz/hoc-element-affix/tree/main/example/src/views/ExampleAffix.vue )
```html
<template>
<div class="box-container">
<div class="content">
<div class="long-list">
<div
v-for="(item, index) in 50"
:key="index"
class="long"
>
<template
v-if="index === 2"
>
<hoc-el-affix>
<template v-slot="{ affixed }">
<div class="box">
<span style="font-size: 25px">{{ affixed ? '🍎' : '🍏' }}</span>
吸顶【插槽版】
</div>
</template>
</hoc-el-affix>
</template>
<template
v-else-if="index === 9"
>
<hoc-el-affix
:offset-top="120"
@change="handleAffixed120"
>
<div class="box">
<span style="font-size: 25px">{{ isAffixed120 ? '🌝' : '🌚' }}</span>
距离顶部120px时固定【回调版】
</div>
</hoc-el-affix>
</template>
<template
v-else
>
{{ index === 49 ? '到底了' : `占位符${index + 1}` }}
</template>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ExampleAffix',
data () {
return {
isAffixed120: false
}
},
methods: {
handleAffixed120 (affixed) {
this.isAffixed120 = affixed
}
}
}
</script>
<style lang="scss" scoped>
.box-container {
.content {
position: relative;
width: 300px;
margin: 0 auto;
text-align: center;
.box {
width: 300px;
height: 50px;
line-height: 50px;
border: 1px solid #000;
}
.long-list {
.long {
height: 50px;
line-height: 50px;
font-size: 16px;
}
}
}
}
</style>
```

View File

@@ -5,7 +5,7 @@ const babelConfig = {
{ {
targets: { targets: {
chrome: '58', chrome: '58',
ie: '6' ie: '11'
} }
} }
] ]
@@ -22,7 +22,8 @@ const babelConfig = {
], ],
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-import-meta', '@babel/plugin-syntax-import-meta',
['@babel/plugin-proposal-class-properties', { loose: true }], ['@babel/plugin-proposal-class-properties', { loose: false }],
['@babel/plugin-proposal-private-methods', { loose: false }],
'@babel/plugin-proposal-json-strings', '@babel/plugin-proposal-json-strings',
[ [
'@babel/plugin-proposal-decorators', '@babel/plugin-proposal-decorators',

7
build/utils.js Normal file
View File

@@ -0,0 +1,7 @@
const path = require('path')
module.exports = {
resolve (dir = '') {
return path.join(process.cwd(), dir)
}
}

View File

@@ -1,13 +1,8 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader') const { VueLoaderPlugin } = require('vue-loader')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const notifier = require('node-notifier')
function resolve (dir) {
return path.join(process.cwd(), dir)
}
const { resolve } = require('./utils')
module.exports = { module.exports = {
mode: 'production', mode: 'production',
entry: './src/main.js', entry: './src/main.js',
@@ -17,21 +12,9 @@ module.exports = {
filename: 'hoc-el-affix.js', filename: 'hoc-el-affix.js',
libraryTarget: 'umd' libraryTarget: 'umd'
}, },
stats: 'minimal',
module: { module: {
rules: [ rules: [
{
enforce: 'pre',
test: /\.(vue|js)(\?.*)?$/,
loader: 'eslint-loader',
include: resolve('src'),
options: {
fix: true,
failOnError: true,
useEslintrc: true,
configFile: resolve('.eslintrc.js'),
formatter: require('eslint-friendly-formatter')
}
},
{ {
test: /\.css$/, test: /\.css$/,
use: [ use: [
@@ -61,7 +44,7 @@ module.exports = {
loader: 'vue-loader' loader: 'vue-loader'
}, },
exclude: /node_modules/, exclude: /node_modules/,
include: resolve('src') include: resolve('./src')
}, },
{ {
test: /\.js$/, test: /\.js$/,
@@ -69,13 +52,42 @@ module.exports = {
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
extends: resolve('babel.config.js') extends: resolve('./babel.config.js')
} }
} }
}, },
{ {
test: /\.(png|jpg|gif|svg)$/, test: /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/,
loader: 'url-loader',
exclude: /node_modules/,
options: {
limit: 10000,
esModule: false,
name: '[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
exclude: /node_modules/,
options: {
limit: 10000,
name: '[name].[hash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
exclude: /node_modules/,
options: {
limit: 10000,
name: '[name].[hash:7].[ext]'
}
},
{
test: /\.(png|jpe?g|gif|svg|webp|woff2?|eot|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac)(\?\S*)?$/,
loader: 'file-loader', loader: 'file-loader',
include: /node_modules/,
options: { options: {
name: '[name].[ext]?[hash]' name: '[name].[ext]?[hash]'
} }
@@ -84,8 +96,8 @@ module.exports = {
}, },
resolve: { resolve: {
alias: { alias: {
vue$: 'vue/dist/vue.esm.js', vue$: 'vue/dist/vue.esm-bundler.js',
'@': resolve('src') '@': resolve('./src')
}, },
extensions: ['*', '.js', '.vue', '.json'] extensions: ['*', '.js', '.vue', '.json']
}, },
@@ -100,24 +112,17 @@ module.exports = {
hints: false hints: false
}, },
devtool: 'source-map', devtool: 'source-map',
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue'
}
},
plugins: [ plugins: [
new VueLoaderPlugin(), new VueLoaderPlugin(),
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
minimize: true minimize: true
}), }),
new FriendlyErrorsWebpackPlugin({
clearConsole: false,
onErrors: (severity, errors) => {
if (severity !== 'error') {
return
}
const error = errors[0]
notifier.notify({
title: 'Webpack error',
message: `${severity}: ${error.name}`,
subtitle: error.file || ''
})
}
})
] ]
} }

View File

@@ -1,48 +1,43 @@
const path = require('path'); const webpack = require('webpack')
const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') const ESLintPlugin = require('eslint-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin'); const { resolve } = require('./utils')
function resolve (dir) {
return path.join(process.cwd(), dir)
}
/**
* @type { webpack.Configuration }
*/
const webpackConfig = { const webpackConfig = {
mode: process.env.NODE_ENV, mode: process.env.NODE_ENV,
target: 'web',
entry: './example/main.js', entry: './example/main.js',
output: { output: {
path: resolve('./example/dist'), path: resolve('./example/dist'),
filename: '[name].[hash:7].js', filename: '[name].[fullhash:7].js'
}, },
resolve: { resolve: {
alias: { alias: {
vue$: 'vue/dist/vue.esm.js', root: resolve(),
'source': resolve('./src'), vue$: 'vue/dist/vue.esm-bundler.js',
'@': resolve('./example/src') '@': resolve('./src'),
example: resolve('./example/src')
// Non-essential, turn it on when using npm link
// vue: resolve('./node_modules/vue')
}, },
extensions: ['*', '.js', '.vue', '.json'] extensions: ['*', '.js', '.vue', '.json']
// Non-essential, turn it on when using npm link
// symlinks: false
}, },
devServer: { devServer: {
publicPath: '/', port: 8086,
port: 8085, historyApiFallback: true,
quiet: true,
hot: true,
open: true,
openPage: 'affix-example'
}, },
performance: { performance: {
hints: false hints: false
}, },
module: { module: {
rules: [ rules: [
{
enforce: 'pre',
test: /\.(vue|jsx?)$/,
exclude: /node_modules/,
loader: 'eslint-loader'
},
{ {
test: /\.(jsx?|babel|es6)$/, test: /\.(jsx?|babel|es6)$/,
include: process.cwd(), include: process.cwd(),
@@ -64,27 +59,68 @@ const webpackConfig = {
} }
}, },
{ {
test: /\.(scss|css)$/, test: /\.css$/,
use: [ use: [
'vue-style-loader', 'vue-style-loader',
'css-loader', 'css-loader'
'sass-loader'
] ]
}, },
{ {
test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/, test: /\.s(c|a)ss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
// Prefer `dart-sass`
implementation: require('sass')
}
}
]
},
{
test: /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
query: { exclude: /node_modules/,
options: {
limit: 10000, limit: 10000,
name: '[name].[hash:7].[ext]' esModule: false,
name: '[name].[fullhash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
exclude: /node_modules/,
options: {
limit: 10000,
name: '[name].[fullhash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
exclude: /node_modules/,
options: {
limit: 10000,
name: '[name].[fullhash:7].[ext]'
}
},
{
test: /\.(png|jpe?g|gif|svg|webp|woff2?|eot|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac)(\?\S*)?$/,
loader: 'file-loader',
include: /node_modules/,
options: {
name: '[name].[ext]?[fullhash]'
} }
} }
] ]
}, },
plugins: [ plugins: [
new webpack.HotModuleReplacementPlugin(), new ESLintPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './example/index.html', template: './example/index.html'
}), }),
new VueLoaderPlugin(), new VueLoaderPlugin(),
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
@@ -94,25 +130,15 @@ const webpackConfig = {
} }
} }
}), }),
new FriendlyErrorsWebpackPlugin({ new webpack.DefinePlugin({
clearConsole: false, __VUE_OPTIONS_API__: false,
onErrors: (severity, errors) => { __VUE_PROD_DEVTOOLS__: false,
if (severity !== 'error') { }),
return
}
const error = errors[0]
notifier.notify({
title: 'Webpack error',
message: `${severity}: ${error.name}`,
subtitle: error.file || ''
})
}
})
], ],
optimization: { optimization: {
minimizer: [] minimizer: []
}, },
devtool: '#eval-source-map' devtool: 'eval-source-map'
}; }
module.exports = webpackConfig; module.exports = webpackConfig

View File

@@ -1,13 +1,13 @@
<template> <template>
<div> <router-view v-slot="{ Component }">
<router-view /> <component :is="Component" />
</div> </router-view>
</template> </template>
<script> <script>
export default { import { defineComponent } from 'vue'
name: 'App'
}
</script>
<style lang="scss">
</style> export default defineComponent({
name: 'App'
})
</script>

View File

@@ -1,12 +1,13 @@
import Vue from 'vue' import { createApp } from 'vue'
import router from './router.js' import router from './router.js'
import HocElementAffix from 'source/main.js'
import App from './App.vue' import App from './App.vue'
Vue.use(HocElementAffix) import HocElementAffix from 'root/lib/hoc-el-affix' // Switch to bundle lib
// import HocElementAffix from '@/main.js'
new Vue({ import 'example/styles/variables.scss'
router,
render: h => h(App) createApp(App)
}).$mount('#app') .use(router)
.use(HocElementAffix)
.mount('#app')

View File

@@ -1,24 +1,15 @@
import Vue from 'vue' import { createRouter, createWebHistory } from 'vue-router'
import VueRouter from 'vue-router'
const Layout = () => import('@/components/Layout')
const importModule = (filePath) => { const importModule = (filePath) => {
return () => import(`@/${filePath}`) return () => import(`example/${filePath}`)
} }
const routes = [{ const routes = [{
path: '/', path: '/',
component: Layout,
children: [
{
path: 'affix-example',
component: importModule('views/ExampleAffix') component: importModule('views/ExampleAffix')
}
]
}] }]
Vue.use(VueRouter) export default createRouter({
export default new VueRouter({
routes, routes,
mode: 'history' history: createWebHistory()
}) })

View File

@@ -0,0 +1,23 @@
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:svgjs="http://svgjs.com/svgjs"
width="32"
height="32"
x="0"
y="0"
viewBox="0 0 24 24"
style="enable-background:new 0 0 32 32"
xml:space="preserve"
><g><path
xmlns="http://www.w3.org/2000/svg"
d="m12 1c-6.354 0-11.5 5.05-11.5 11.279 0 4.984 3.295 9.211 7.863 10.701.575.106.786-.243.786-.542 0-.268-.01-.978-.014-1.917-3.199.681-3.874-1.513-3.874-1.513-.523-1.302-1.279-1.65-1.279-1.65-1.042-.699.08-.685.08-.685 1.155.079 1.761 1.162 1.761 1.162 1.025 1.725 2.692 1.227 3.349.938.104-.729.4-1.227.728-1.509-2.554-.282-5.238-1.252-5.238-5.574 0-1.231.446-2.237 1.184-3.027-.129-.284-.517-1.431.101-2.985 0 0 .963-.303 3.162 1.156 1.775-.484 3.832-.523 5.75 0 2.185-1.459 3.148-1.156 3.148-1.156.618 1.554.23 2.7.115 2.985.733.79 1.179 1.795 1.179 3.027 0 4.333-2.688 5.287-5.247 5.564 1.035.87.762 2.494.762 5.175 0 .296.201.649.791.536 4.601-1.479 7.893-5.709 7.893-10.686 0-6.229-5.149-11.279-11.5-11.279z"
fill="#000000"
data-original="#b3b3b3"
/><path
xmlns="http://www.w3.org/2000/svg"
d="m.184 10.462c-.779 4.906 1.401 10.823 8.123 13.006.12.022.231.032.335.032.782 0 1.32-.582 1.32-1.3-.097-.523.383-2.642-.92-2.357-2.519.536-2.821-.871-3.205-1.607 1.086 1.394 2.718 1.359 3.949.819.683-.3.326-1.064.65-1.343.496-.426.244-1.243-.407-1.314-2.314-.255-4.457-1.001-4.457-4.702 0-2.168 1.505-2.362 1.09-3.269-.015-.033-.333-.754-.045-1.849 1.419.262 2.072 1.28 2.753 1.097 1.687-.46 3.544-.46 5.23 0 .704.189 1.207-.801 2.738-1.103.441 1.654-.473 2.058.103 2.677.632.68.953 1.503.953 2.447 0 5.564-4.717 3.957-5.101 5.22-.088.288.005.599.235.792.61.513.53 1.83.465 2.889-.067 1.098-.125 2.045.482 2.579.214.19.595.393 1.284.253 6.634-2.131 8.83-8.022 8.063-12.917-2.096-13.368-21.526-13.352-23.638-.05zm8.27 10.978.004.505c-.523-.181-1.015-.39-1.475-.623.425.109.913.156 1.471.118zm.37-3.7c-.005.026-.01.053-.015.08-.853.252-1.509.001-1.957-.752 0-.001 0-.001-.001-.002.68.364 1.381.56 1.973.674zm3.176-15.74c11.833 0 14.502 16.267 3.469 19.941-.038-.297-.003-.857.021-1.252.058-.951.126-2.059-.213-2.985 5.088-1.059 5.513-6.646 3.554-9.135.243-.952.145-3.189-.729-3.463-.206-.065-1.305-.304-3.437 1.037-1.741-.416-3.62-.417-5.361 0-1.064-.667-3.462-1.752-3.922-.6-.534 1.342-.407 2.427-.248 3.03-1.739 2.204-1.218 5.894.534 7.626-.993-.475-2.361-.637-2.656.314-.323 1.037.912.911 1.679 2.804.073.236.208.513.415.788-6.811-5.565-3.525-18.105 6.894-18.105z"
fill="#ffffff"
data-original="#000000"
/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,28 +1,12 @@
<template> <template>
<div> <router-view v-slot="{ Component }">
<router-view /> <component :is="Component" />
</div> </router-view>
</template> </template>
<script> <script>
export default { import { defineComponent } from 'vue'
export default defineComponent({
name: 'Layout' name: 'Layout'
} })
</script> </script>
<style>
* {
margin: 0;
padding: 0;
}
body {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
html {
box-sizing: border-box;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,14 @@
* {
margin: 0;
padding: 0;
}
html, body {
background-color: #000;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #FCFAF2;
}

View File

@@ -1,5 +1,18 @@
<template> <template>
<div class="box-container"> <div class="box-container">
<div class="banner">
<div class="repo">
<p>📌 @hoc-element/affix</p><a
href="https://github.com/pdsuwwz/hoc-element-affix"
target="_blank"
>
<img
src="~example/assets/logo.svg"
alt="Logo"
>
</a>
</div>
</div>
<div class="content"> <div class="content">
<div class="long-list"> <div class="long-list">
<div <div
@@ -11,9 +24,9 @@
v-if="index === 2" v-if="index === 2"
> >
<hoc-el-affix> <hoc-el-affix>
<template v-slot="{ affixed }"> <template #default="{ affixed }">
<div class="box"> <div class="box">
<span style="font-size: 25px">{{ affixed ? '🍎' : '🍏' }}</span> <span style="font-size: 25px">{{ affixed ? '🥳' : '😀' }}</span>
吸顶插槽版 吸顶插槽版
</div> </div>
</template> </template>
@@ -27,15 +40,15 @@
@change="handleAffixed120" @change="handleAffixed120"
> >
<div class="box"> <div class="box">
<span style="font-size: 25px">{{ isAffixed120 ? '🌝' : '🌚' }}</span> <span style="font-size: 25px">{{ isAffixed120 ? '🍎' : '🍏' }}</span>
距离顶部120px时固定回调版 距离顶部 120px 时固定回调版
</div> </div>
</hoc-el-affix> </hoc-el-affix>
</template> </template>
<template <template
v-else v-else
> >
{{ index === 49 ? '到底了' : `占位符${index + 1}` }} {{ index === 49 ? '🎊 到底了' : `占位符${index + 1}` }}
</template> </template>
</div> </div>
</div> </div>
@@ -44,23 +57,51 @@
</template> </template>
<script> <script>
export default { import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'ExampleAffix', name: 'ExampleAffix',
data () { setup () {
const isAffixed120 = ref(false)
const handleAffixed120 = (affixed) => {
isAffixed120.value = affixed
}
return { return {
isAffixed120: false isAffixed120,
} handleAffixed120
},
methods: {
handleAffixed120 (affixed) {
this.isAffixed120 = affixed
} }
} }
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.box-container { .box-container {
.banner {
padding: 30px 0;
background-color: #1C1C1C;
.repo {
margin: 0 auto;
display: flex;
justify-content: space-around;
align-items: center;
text-align: center;
font-size: 30px;
font-weight: 700;
a {
color: #000;
text-decoration: none;
}
}
}
@media screen and (min-width: 400px) {
.banner {
.repo {
width: 400px;
}
}
}
.content { .content {
position: relative; position: relative;
width: 300px; width: 300px;
@@ -70,7 +111,7 @@ export default {
width: 300px; width: 300px;
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;
border: 1px solid #000; border: 1px solid #FCFAF2;
} }
.long-list { .long-list {
.long { .long {

12
jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs",
"paths": {
"root/*": ["./*"],
"@/*": ["./src/*"],
"example/*": ["./example/src/*"]
}
},
"exclude": ["node_modules"]
}

View File

@@ -6,8 +6,8 @@
"author": "Wisdom <pdsu.wwz@foxmail.com>", "author": "Wisdom <pdsu.wwz@foxmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js --progress --colors --hide-modules", "build": "cross-env NODE_ENV=production webpack --progress --config build/webpack.config.js",
"dev:example": "cross-env NODE_ENV=development webpack-dev-server --display-error-details --progress --colors --history-api-fallback --config build/webpack.example.js" "dev:example": "cross-env NODE_ENV=development webpack serve --progress --config build/webpack.example.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -16,54 +16,57 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"dependencies": { "peerDependencies": {
"vue": "^2.6.12" "vue": ">=3.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.6", "@babel/core": "^7.16.5",
"@babel/eslint-parser": "^7.11.5", "@babel/eslint-parser": "^7.16.5",
"@babel/eslint-plugin": "^7.11.5", "@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.16.5",
"@babel/plugin-proposal-decorators": "^7.1.2", "@babel/plugin-proposal-decorators": "^7.16.5",
"@babel/plugin-proposal-export-default-from": "^7.0.0", "@babel/plugin-proposal-export-default-from": "^7.16.5",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0", "@babel/plugin-proposal-export-namespace-from": "^7.16.5",
"@babel/plugin-proposal-function-sent": "^7.0.0", "@babel/plugin-proposal-function-sent": "^7.16.5",
"@babel/plugin-proposal-json-strings": "^7.0.0", "@babel/plugin-proposal-json-strings": "^7.16.5",
"@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-numeric-separator": "^7.16.5",
"@babel/plugin-proposal-throw-expressions": "^7.0.0", "@babel/plugin-proposal-private-methods": "^7.16.5",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-proposal-throw-expressions": "^7.16.5",
"@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.1.0", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.0.0", "@babel/plugin-transform-modules-commonjs": "^7.16.5",
"@babel/preset-env": "^7.0.0", "@babel/plugin-transform-runtime": "^7.16.5",
"@babel/runtime": "^7.1.2", "@babel/preset-env": "^7.16.5",
"@babel/runtime-corejs2": "^7.1.2", "@babel/runtime": "^7.16.5",
"@vue/eslint-config-standard": "^5.1.2", "@babel/runtime-corejs2": "^7.16.5",
"babel-loader": "8.0.0", "@vue/compiler-sfc": "^3.2.26",
"cross-env": "^5.0.5", "@vue/eslint-config-standard": "^6.1.0",
"css-loader": "^0.28.7", "babel-loader": "^8.2.3",
"eslint": "^7.10.0", "core-js": "^3.20.1",
"eslint-config-standard": "^14.1.0", "cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^3.0.3", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-import": "^2.19.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^5.2.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-vue": "^7.20.0",
"eslint-plugin-standard": "^4.0.1", "eslint-webpack-plugin": "^3.1.1",
"eslint-plugin-vue": "^6.1.2", "file-loader": "^6.2.0",
"file-loader": "^1.1.4", "html-webpack-plugin": "^5.5.0",
"friendly-errors-webpack-plugin": "^1.7.0", "sass": "^1.45.1",
"html-webpack-plugin": "^4.5.0", "sass-loader": "^8.0.2",
"node-notifier": "^8.0.1", "style-loader": "^3.3.1",
"node-sass": "^4.5.3", "url-loader": "^4.1.1",
"sass-loader": "^6.0.6", "vue": "^3.2.26",
"vue-loader": "^15.8.3", "vue-loader": "^16.8.3",
"vue-router": "^3.4.5", "vue-router": "^4.0.12",
"vue-template-compiler": "^2.6.12", "vue-style-loader": "^4.1.3",
"webpack": "^4.41.5", "webpack": "^5.65.0",
"webpack-cli": "^3.3.10", "webpack-cli": "^4.9.1",
"webpack-dev-server": "^3.11.0" "webpack-dev-server": "^4.7.2"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
@@ -72,7 +75,6 @@
], ],
"keywords": [ "keywords": [
"vue", "vue",
"element-ui",
"hoc", "hoc",
"affix", "affix",
"固钉", "固钉",

5548
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
:style="stylePlaceHolder" :style="stylePlaceHolder"
/> />
<div <div
ref="scroll-affix" ref="refScrollAffix"
:style="affixStyle" :style="affixStyle"
class="scroll-affix-container" class="scroll-affix-container"
> >
@@ -19,7 +19,9 @@
</template> </template>
<script> <script>
export default { import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
export default defineComponent({
name: 'Affix', name: 'Affix',
props: { props: {
offsetTop: { offsetTop: {
@@ -27,105 +29,110 @@ export default {
default: 0 default: 0
} }
}, },
data () { emits: ['change'],
return { setup (props, { emit }) {
affixStyle: { // ref 实例
const refScrollAffix = ref({})
const affixStyle = ref({
position: 'initial', position: 'initial',
top: 'initial' top: 'initial'
},
// 占位区域样式
stylePlaceHolder: {},
// 占位区域
showPlaceHolder: false,
// 实例
instance: '',
// 用于记录实例的初始状态下的位置
defaultInstancePosition: ''
}
},
computed: {
getAffixed () {
return this.affixStyle.position === 'fixed'
}
},
mounted () {
this.$nextTick(() => {
this.instance = this.$refs['scroll-affix']
this.createAffix()
}) })
}, // 占位区域样式
beforeDestroy () { const stylePlaceHolder = ref({})
document.removeEventListener('scroll', this.scrollListener) // 占位区域
}, const showPlaceHolder = ref(false)
methods: { // 用于记录实例的初始状态下的位置
getInstanceRect () { const defaultInstancePosition = ref('')
return this.instance.getBoundingClientRect()
},
getWindowScrollTop () { const getAffixed = computed(() => affixStyle.value.position === 'fixed')
onMounted(() => {
nextTick(() => {
createAffix()
})
})
onBeforeUnmount(() => {
document.removeEventListener('scroll', scrollListener)
})
const getInstanceRect = () => {
return refScrollAffix.value.getBoundingClientRect()
}
const getWindowScrollTop = () => {
return window.pageYOffset || return window.pageYOffset ||
document.documentElement.scrollTop || document.documentElement.scrollTop ||
document.body.scrollTop document.body.scrollTop
},
createAffix () {
this.defaultInstancePosition = this.getInstanceRect().top
this.beforeListener()
document.addEventListener('scroll', this.scrollListener)
},
setFixedForInstance () {
this.affixStyle = {
position: 'fixed',
top: `${this.offsetTop}px`
} }
this.$emit('change', true)
}, const createAffix = () => {
defaultInstancePosition.value = getInstanceRect().top
beforeListener()
document.addEventListener('scroll', scrollListener)
}
const setFixedForInstance = () => {
affixStyle.value = {
position: 'fixed',
top: `${props.offsetTop}px`
}
emit('change', true)
}
// 用于设置实例在固定后的空白占位 // 用于设置实例在固定后的空白占位
setPlaceHolder () { const setPlaceHolder = () => {
this.showPlaceHolder = true showPlaceHolder.value = true
const instanceRect = this.getInstanceRect() const instanceRect = getInstanceRect()
this.stylePlaceHolder = { stylePlaceHolder.value = {
width: `${instanceRect.width}px`, width: `${instanceRect.width}px`,
height: `${instanceRect.height}px` height: `${instanceRect.height}px`
} }
}, }
beforeListener () {
const beforeListener = () => {
// 若下一次进入页面发现滚动条所处位置已经超过了实例,则立即固定 // 若下一次进入页面发现滚动条所处位置已经超过了实例,则立即固定
if (this.defaultInstancePosition < this.offsetTop) { if (defaultInstancePosition.value < props.offsetTop) {
this.setFixedForInstance() setFixedForInstance()
this.setPlaceHolder() setPlaceHolder()
} }
this.defaultInstancePosition = this.getWindowScrollTop() + this.defaultInstancePosition defaultInstancePosition.value = getWindowScrollTop() + defaultInstancePosition.value
}, }
scrollListener () {
const offsetTop = this.getInstanceRect().top const scrollListener = () => {
const offsetTop = getInstanceRect().top
// 当实例距离顶部的距离刚好接近(0px+设置的 top 距离)时,则立即固定 // 当实例距离顶部的距离刚好接近(0px+设置的 top 距离)时,则立即固定
if (offsetTop < this.offsetTop) { if (offsetTop < props.offsetTop) {
this.setFixedForInstance() setFixedForInstance()
this.setPlaceHolder() setPlaceHolder()
} }
const windowScrollTop = this.getWindowScrollTop() const windowScrollTop = getWindowScrollTop()
const isArrivalDefault = (this.defaultInstancePosition - this.offsetTop) >= windowScrollTop const isArrivalDefault = (defaultInstancePosition.value - props.offsetTop) >= windowScrollTop
// 当实例的初始位置减去设置的 top 距离刚好接近滚动条滚过的距离时(即一直在向上滚动)&& 实例已经在固定状态,则取消固定 // 当实例的初始位置减去设置的 top 距离刚好接近滚动条滚过的距离时(即一直在向上滚动)&& 实例已经在固定状态,则取消固定
if (isArrivalDefault && this.affixStyle.position === 'fixed') { if (isArrivalDefault && affixStyle.value.position === 'fixed') {
this.affixStyle = {} affixStyle.value = {}
this.showPlaceHolder = false showPlaceHolder.value = false
this.stylePlaceHolder = {} stylePlaceHolder.value = {}
this.$emit('change', false) emit('change', false)
} }
} }
return {
refScrollAffix,
affixStyle,
stylePlaceHolder,
showPlaceHolder,
defaultInstancePosition,
getAffixed
} }
} }
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
import HocElAffix from './components/ScrollAffix' import HocElAffix from '@/components/ScrollAffix'
const install = function (Vue, opts = {}) { const install = function (app) {
Vue.component('HocElAffix', HocElAffix) app.component('HocElAffix', HocElAffix)
} }
if (typeof window !== 'undefined' && window.Vue) { if (typeof window !== 'undefined' && window.Vue) {

8282
yarn.lock

File diff suppressed because it is too large Load Diff