Merge pull request #22 from pdsuwwz/feature/vue3-support
🌈 feat: upgrade to Vue 3
This commit is contained in:
118
README.md
118
README.md
@@ -2,34 +2,49 @@
|
||||
|
||||
 
|
||||
|
||||
📌 将页面元素固定在可视范围内。
|
||||
📌 基于 Webpack 5 构建的 Vue 3.x 固钉组件,用于将页面元素固定在可视范围内。
|
||||
|
||||
**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
|
||||
|
||||
* Vue 2.6.0+
|
||||
* Vue 3.2.x
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
npm install @hoc-element/affix
|
||||
|
||||
# or
|
||||
|
||||
pnpm add @hoc-element/affix
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
import HocElementAffix from '@hoc-element/affix'
|
||||
import App from './App.vue'
|
||||
|
||||
Vue.use(HocElementAffix)
|
||||
createApp(App)
|
||||
.use(HocElementAffix)
|
||||
.mount('#app')
|
||||
```
|
||||
|
||||
## Feature
|
||||
|
||||
- [x] 自定义顶部偏移量
|
||||
- [x] 固定状态改变时触发的回调
|
||||
- [x] 插槽式的固定状态反馈
|
||||
- [x] 支持 Vue 3.x
|
||||
- [x] 支持自定义顶部 `offsetTop` 偏移量
|
||||
- [x] 支持固定状态改变触发回调
|
||||
- [x] 支持 `Slot` 插槽式的固定状态反馈
|
||||
|
||||
## Using
|
||||
|
||||
@@ -56,92 +71,5 @@ Vue.use(HocElementAffix)
|
||||
|
||||
## Demo
|
||||
|
||||
下面是比较全的例子,几乎囊括了 API 的所有用法,源码戳这: [Code](https://github.com/pdsuwwz/hoc-element-affix/tree/master/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>
|
||||
```
|
||||
下面是比较全的例子,几乎囊括了 API 的所有用法,源码戳这: [Code](https://github.com/pdsuwwz/hoc-element-affix/tree/main/example/src/views/ExampleAffix.vue )
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const babelConfig = {
|
||||
{
|
||||
targets: {
|
||||
chrome: '58',
|
||||
ie: '6'
|
||||
ie: '11'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -22,7 +22,8 @@ const babelConfig = {
|
||||
],
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@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-decorators',
|
||||
|
||||
7
build/utils.js
Normal file
7
build/utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
resolve (dir = '') {
|
||||
return path.join(process.cwd(), dir)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
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 = {
|
||||
mode: 'production',
|
||||
entry: './src/main.js',
|
||||
@@ -17,21 +12,9 @@ module.exports = {
|
||||
filename: 'hoc-el-affix.js',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
stats: 'minimal',
|
||||
module: {
|
||||
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$/,
|
||||
use: [
|
||||
@@ -61,7 +44,7 @@ module.exports = {
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
include: resolve('src')
|
||||
include: resolve('./src')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
@@ -69,13 +52,42 @@ module.exports = {
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
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',
|
||||
include: /node_modules/,
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
@@ -84,8 +96,8 @@ module.exports = {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.esm.js',
|
||||
'@': resolve('src')
|
||||
vue$: 'vue/dist/vue.esm-bundler.js',
|
||||
'@': resolve('./src')
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
@@ -100,24 +112,17 @@ module.exports = {
|
||||
hints: false
|
||||
},
|
||||
devtool: 'source-map',
|
||||
externals: {
|
||||
vue: {
|
||||
root: 'Vue',
|
||||
commonjs: 'vue',
|
||||
commonjs2: 'vue'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
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 || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,48 +1,43 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(process.cwd(), dir)
|
||||
}
|
||||
const { resolve } = require('./utils')
|
||||
|
||||
/**
|
||||
* @type { webpack.Configuration }
|
||||
*/
|
||||
const webpackConfig = {
|
||||
mode: process.env.NODE_ENV,
|
||||
target: 'web',
|
||||
entry: './example/main.js',
|
||||
output: {
|
||||
path: resolve('./example/dist'),
|
||||
filename: '[name].[hash:7].js',
|
||||
filename: '[name].[fullhash:7].js'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.esm.js',
|
||||
'source': resolve('./src'),
|
||||
'@': resolve('./example/src')
|
||||
root: resolve(),
|
||||
vue$: 'vue/dist/vue.esm-bundler.js',
|
||||
'@': resolve('./src'),
|
||||
example: resolve('./example/src')
|
||||
// Non-essential, turn it on when using npm link
|
||||
// vue: resolve('./node_modules/vue')
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
// Non-essential, turn it on when using npm link
|
||||
// symlinks: false
|
||||
},
|
||||
devServer: {
|
||||
publicPath: '/',
|
||||
port: 8085,
|
||||
quiet: true,
|
||||
hot: true,
|
||||
open: true,
|
||||
openPage: 'affix-example'
|
||||
port: 8086,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.(vue|jsx?)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(jsx?|babel|es6)$/,
|
||||
include: process.cwd(),
|
||||
@@ -64,27 +59,68 @@ const webpackConfig = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(scss|css)$/,
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
'css-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',
|
||||
query: {
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
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: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new ESLintPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './example/index.html',
|
||||
template: './example/index.html'
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
@@ -94,25 +130,15 @@ const webpackConfig = {
|
||||
}
|
||||
}
|
||||
}),
|
||||
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 || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: false,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
minimizer: []
|
||||
},
|
||||
devtool: '#eval-source-map'
|
||||
};
|
||||
devtool: 'eval-source-map'
|
||||
}
|
||||
|
||||
module.exports = webpackConfig;
|
||||
module.exports = webpackConfig
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
</style>
|
||||
export default defineComponent({
|
||||
name: 'App'
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
import router from './router.js'
|
||||
|
||||
import HocElementAffix from 'source/main.js'
|
||||
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({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
import 'example/styles/variables.scss'
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(HocElementAffix)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const Layout = () => import('@/components/Layout')
|
||||
const importModule = (filePath) => {
|
||||
return () => import(`@/${filePath}`)
|
||||
return () => import(`example/${filePath}`)
|
||||
}
|
||||
|
||||
const routes = [{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'affix-example',
|
||||
component: importModule('views/ExampleAffix')
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
Vue.use(VueRouter)
|
||||
export default new VueRouter({
|
||||
export default createRouter({
|
||||
routes,
|
||||
mode: 'history'
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
23
example/src/assets/logo.svg
Normal file
23
example/src/assets/logo.svg
Normal 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 |
@@ -1,28 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Layout'
|
||||
}
|
||||
})
|
||||
</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>
|
||||
|
||||
14
example/src/styles/variables.scss
Normal file
14
example/src/styles/variables.scss
Normal 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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<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="long-list">
|
||||
<div
|
||||
@@ -11,9 +24,9 @@
|
||||
v-if="index === 2"
|
||||
>
|
||||
<hoc-el-affix>
|
||||
<template v-slot="{ affixed }">
|
||||
<template #default="{ affixed }">
|
||||
<div class="box">
|
||||
<span style="font-size: 25px">{{ affixed ? '🍎' : '🍏' }}</span>
|
||||
<span style="font-size: 25px">{{ affixed ? '🥳' : '😀' }}</span>
|
||||
吸顶【插槽版】
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,15 +40,15 @@
|
||||
@change="handleAffixed120"
|
||||
>
|
||||
<div class="box">
|
||||
<span style="font-size: 25px">{{ isAffixed120 ? '🌝' : '🌚' }}</span>
|
||||
距离顶部120px时固定【回调版】
|
||||
<span style="font-size: 25px">{{ isAffixed120 ? '🍎' : '🍏' }}</span>
|
||||
距离顶部 120px 时固定【回调版】
|
||||
</div>
|
||||
</hoc-el-affix>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
{{ index === 49 ? '到底了' : `占位符${index + 1}` }}
|
||||
{{ index === 49 ? '🎊 到底了' : `占位符${index + 1}` }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,23 +57,51 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ExampleAffix',
|
||||
data () {
|
||||
setup () {
|
||||
const isAffixed120 = ref(false)
|
||||
|
||||
const handleAffixed120 = (affixed) => {
|
||||
isAffixed120.value = affixed
|
||||
}
|
||||
|
||||
return {
|
||||
isAffixed120: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAffixed120 (affixed) {
|
||||
this.isAffixed120 = affixed
|
||||
isAffixed120,
|
||||
handleAffixed120
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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 {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
@@ -70,7 +111,7 @@ export default {
|
||||
width: 300px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border: 1px solid #000;
|
||||
border: 1px solid #FCFAF2;
|
||||
}
|
||||
.long-list {
|
||||
.long {
|
||||
|
||||
12
jsconfig.json
Normal file
12
jsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"paths": {
|
||||
"root/*": ["./*"],
|
||||
"@/*": ["./src/*"],
|
||||
"example/*": ["./example/src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
98
package.json
98
package.json
@@ -6,8 +6,8 @@
|
||||
"author": "Wisdom <pdsu.wwz@foxmail.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js --progress --colors --hide-modules",
|
||||
"dev:example": "cross-env NODE_ENV=development webpack-dev-server --display-error-details --progress --colors --history-api-fallback --config build/webpack.example.js"
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --config build/webpack.config.js",
|
||||
"dev:example": "cross-env NODE_ENV=development webpack serve --progress --config build/webpack.example.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,54 +16,57 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^2.6.12"
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/eslint-parser": "^7.11.5",
|
||||
"@babel/eslint-plugin": "^7.11.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.1.2",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-function-sent": "^7.0.0",
|
||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.1.0",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@babel/runtime-corejs2": "^7.1.2",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"babel-loader": "8.0.0",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-config-standard": "^14.1.0",
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/eslint-parser": "^7.16.5",
|
||||
"@babel/eslint-plugin": "^7.16.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.5",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.16.5",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.16.5",
|
||||
"@babel/plugin-proposal-function-sent": "^7.16.5",
|
||||
"@babel/plugin-proposal-json-strings": "^7.16.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.16.5",
|
||||
"@babel/plugin-proposal-private-methods": "^7.16.5",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.16.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.16.5",
|
||||
"@babel/plugin-transform-runtime": "^7.16.5",
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"@babel/runtime": "^7.16.5",
|
||||
"@babel/runtime-corejs2": "^7.16.5",
|
||||
"@vue/compiler-sfc": "^3.2.26",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"core-js": "^3.20.1",
|
||||
"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-loader": "^3.0.3",
|
||||
"eslint-plugin-import": "^2.19.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"file-loader": "^1.1.4",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"node-notifier": "^8.0.1",
|
||||
"node-sass": "^4.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"vue-loader": "^15.8.3",
|
||||
"vue-router": "^3.4.5",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"eslint-webpack-plugin": "^3.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"sass": "^1.45.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"style-loader": "^3.3.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue": "^3.2.26",
|
||||
"vue-loader": "^16.8.3",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.7.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
@@ -72,7 +75,6 @@
|
||||
],
|
||||
"keywords": [
|
||||
"vue",
|
||||
"element-ui",
|
||||
"hoc",
|
||||
"affix",
|
||||
"固钉",
|
||||
|
||||
5548
pnpm-lock.yaml
generated
Normal file
5548
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
:style="stylePlaceHolder"
|
||||
/>
|
||||
<div
|
||||
ref="scroll-affix"
|
||||
ref="refScrollAffix"
|
||||
:style="affixStyle"
|
||||
class="scroll-affix-container"
|
||||
>
|
||||
@@ -19,7 +19,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Affix',
|
||||
props: {
|
||||
offsetTop: {
|
||||
@@ -27,105 +29,110 @@ export default {
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
affixStyle: {
|
||||
emits: ['change'],
|
||||
setup (props, { emit }) {
|
||||
// ref 实例
|
||||
const refScrollAffix = ref({})
|
||||
|
||||
const affixStyle = ref({
|
||||
position: '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 () {
|
||||
document.removeEventListener('scroll', this.scrollListener)
|
||||
},
|
||||
methods: {
|
||||
getInstanceRect () {
|
||||
return this.instance.getBoundingClientRect()
|
||||
},
|
||||
// 占位区域样式
|
||||
const stylePlaceHolder = ref({})
|
||||
// 占位区域
|
||||
const showPlaceHolder = ref(false)
|
||||
// 用于记录实例的初始状态下的位置
|
||||
const defaultInstancePosition = ref('')
|
||||
|
||||
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 ||
|
||||
document.documentElement.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 () {
|
||||
this.showPlaceHolder = true
|
||||
const setPlaceHolder = () => {
|
||||
showPlaceHolder.value = true
|
||||
|
||||
const instanceRect = this.getInstanceRect()
|
||||
this.stylePlaceHolder = {
|
||||
const instanceRect = getInstanceRect()
|
||||
stylePlaceHolder.value = {
|
||||
width: `${instanceRect.width}px`,
|
||||
height: `${instanceRect.height}px`
|
||||
}
|
||||
},
|
||||
beforeListener () {
|
||||
}
|
||||
|
||||
const beforeListener = () => {
|
||||
// 若下一次进入页面发现滚动条所处位置已经超过了实例,则立即固定
|
||||
if (this.defaultInstancePosition < this.offsetTop) {
|
||||
this.setFixedForInstance()
|
||||
this.setPlaceHolder()
|
||||
if (defaultInstancePosition.value < props.offsetTop) {
|
||||
setFixedForInstance()
|
||||
setPlaceHolder()
|
||||
}
|
||||
|
||||
this.defaultInstancePosition = this.getWindowScrollTop() + this.defaultInstancePosition
|
||||
},
|
||||
scrollListener () {
|
||||
const offsetTop = this.getInstanceRect().top
|
||||
defaultInstancePosition.value = getWindowScrollTop() + defaultInstancePosition.value
|
||||
}
|
||||
|
||||
const scrollListener = () => {
|
||||
const offsetTop = getInstanceRect().top
|
||||
// 当实例距离顶部的距离刚好接近(0px+设置的 top 距离)时,则立即固定
|
||||
if (offsetTop < this.offsetTop) {
|
||||
this.setFixedForInstance()
|
||||
this.setPlaceHolder()
|
||||
if (offsetTop < props.offsetTop) {
|
||||
setFixedForInstance()
|
||||
setPlaceHolder()
|
||||
}
|
||||
|
||||
const windowScrollTop = this.getWindowScrollTop()
|
||||
const isArrivalDefault = (this.defaultInstancePosition - this.offsetTop) >= windowScrollTop
|
||||
const windowScrollTop = getWindowScrollTop()
|
||||
const isArrivalDefault = (defaultInstancePosition.value - props.offsetTop) >= windowScrollTop
|
||||
|
||||
// 当实例的初始位置减去设置的 top 距离刚好接近滚动条滚过的距离时(即一直在向上滚动)&& 实例已经在固定状态,则取消固定
|
||||
if (isArrivalDefault && this.affixStyle.position === 'fixed') {
|
||||
this.affixStyle = {}
|
||||
this.showPlaceHolder = false
|
||||
this.stylePlaceHolder = {}
|
||||
this.$emit('change', false)
|
||||
if (isArrivalDefault && affixStyle.value.position === 'fixed') {
|
||||
affixStyle.value = {}
|
||||
showPlaceHolder.value = false
|
||||
stylePlaceHolder.value = {}
|
||||
emit('change', false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
refScrollAffix,
|
||||
affixStyle,
|
||||
stylePlaceHolder,
|
||||
showPlaceHolder,
|
||||
defaultInstancePosition,
|
||||
|
||||
getAffixed
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import HocElAffix from './components/ScrollAffix'
|
||||
import HocElAffix from '@/components/ScrollAffix'
|
||||
|
||||
const install = function (Vue, opts = {}) {
|
||||
Vue.component('HocElAffix', HocElAffix)
|
||||
const install = function (app) {
|
||||
app.component('HocElAffix', HocElAffix)
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
|
||||
Reference in New Issue
Block a user