Создание скроллбара с эффектом загрузки на Vue.js

Перевод статьи Creating a Scroll-Based Progress Bar in Vue.

Случаются ситуации когда нам необходимо показать пользователям какую часть статьи они прочитали. И одним из лучших способов является индикатор прогресса с процентами.

Что бы узнать сколько прочитал пользователь, необходимо рассчитать сколько он проскроллил эту статью, что поможет нам высчитать процент прочитанного от всей статьи. И конечно нам необходим компонент что бы отобразить это всё наглядно.

Если вам стало интересно, вы можете ознакомиться с демкой на CodePen, а так же ниже в статье вы найдете информацию о том как это можно реализовать, .

Шаг 1: Создаем приложение Vue

Для реализации нашей задумки я предлагаю использовать мгновенное прототипирование Vue. Создаем файл Vue.app и добавляем в него следующее:

<template>
<div id="app">
<div class="card">
<progress-bar :value="progress"/>
<div class="text-section">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cupiditate architecto voluptatum, laboriosam quisquam quod minus molestias. Rem ut quidem, corrupti nihil molestiae deserunt, iusto velit unde atque mollitia, eum ad maiores exercitationem. Soluta harum sit cupiditate eos, commodi itaque nihil, beatae dolorem ducimus, repudiandae, vero quo corporis sed laborum at maxime dicta dolores perferendis! Possimus repellat velit iste quod recusandae suscipit vitae ex soluta nostrum animi saepe eius itaque, voluptas, sapiente minima quo culpa explicabo necessitatibus distinctio. Veritatis amet tempora, consectetur molestias optio eveniet laudantium, tenetur aspernatur nobis ratione sit hic in impedit quod deserunt recusandae atque, ipsam molestiae sequi!
</div>
</div>
</div>
</template>

Здесь мы выводим небольшой блок с некоторым количеством текста, а так же компонент прогресс бара который мы ещё не создали.

В секцию <script> вставляем инициализацию нашего приложения:

<script>
import ProgressBar from './ProgressBar'

export default {
components: { ProgressBar },
data () {
return {
progress: 0
}
}
}
</script>

Это стандартная инициализация Vue экземпляра, здесь мы добавляем компонент прогресс бара, а так же указываем параметр progress в котором мы будем хранить прогресс чтения пользователя от 0 до 1.

Теперь переходим к секции <style>.

<style>
* {
box-sizing: border-box;
}

body {
margin: 0;
padding: 1px 0 0;
width: 100vw;
height: 100vh;
background: #6B5CA5;
}

html {
padding: 0;
margin: 0;
}

.card {
border-radius: 3px;
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
font-size: 18px;
color: #444;
width: 300px;
margin: 10px auto;
height: 150px;
margin-top: 10px;
display: flex;
flex-direction: column;
background: #FFFFFF;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

.text-section {
height: 100%;
max-height: 100%;
padding: 0 10px 10px;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
</style>

Немного стилей что бы оформить все это безобразие.

Шаг 2: Создание компонента ProgressBar.vue

Создаем файл ProgressBar.vue и добавляем в него следующий код:

<template>
<div class="progress-bar">
<div class="filled-bar"></div>
<span class="percentage-text">
Progress: {{ percentageText }}
</span>
</div>
</template>

Этот компонент состоит из 3 частей:

  • .progress-bar контейнер прогресс бара, здесь мы укажем ему высоту и ширину;
  • .filled-bar имеет ту же ширину и высоту что и контейнер, но заполняет сплошным цветом;
  • .percentage-text отображает текущий процент прочитанного текста.

.filtered-bar и .percentage-text будут спозиционированы абсолютно. И конечно же у .percentage-text будет больший z-index чем у .filtered-bar

Как упоминалось выше, .filled-bar будет иметь такую же ширину, что и его контейнер. Это означает, что мы будем использовать его положение для отображения текущего процента прокрутки. Итак, когда пользователь находится на самом верху (еще не скроллил), мы полностью переместим заполненную строку за пределы контейнера прогресс бара(с левой стороны). А пока пользователь прокручивает страницу вниз, мы переместим заполненную строку вправо, чтобы постепенно войти в контейнер прогресс бара.

Конечно же нам не нужно что бы лишняя часть блока отображалась, поэтому мы добавим overflow: hidden к контейнеру (.progress-bar).

Теперь напишем немного кода в секцию <script>:

<script>
export default {
props: {
value: {
type: Number,
default: 0
}
},

computed: {
percentageText () {
return `${Math.round(this.value * 100)}%`
}
}
}
</script>

Обратите внимание как мы получаем текущее значение прогресса через переменную value.

Используя вычисляемое свойство percentageText мы превращаем дробное значение в целые проценты, то есть если у нас прогресс равен 0.05, то мы получим 5%.

Теперь давайте добавим немного стилей:

<style scoped>
.progress-bar {
position: relative;
height: 30px;
width: 100%;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 3px 3px 0 0;
}

.percentage-text {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
font-size: 14px;
transform: translate3d(-50%, -50%, 0);
}

.filled-bar {
position: absolute;
top: 0;
left: 0;
z-index: 2;
height: 100%;
width: 100%;
background: #eef0ff;
}
</style>

Если у вас открыт браузер при выполнении этих операций, то вы скорее всего увидите что то подобное:

Теперь нам необходимо сделать 2 вещи что бы заставить всё это работать:

  • Поменять положение заполненного контейнера исходя из значения параметраvalue;
  • Посчитать прогресс скролла пользователя в процентах, и передать его в параметр value.

Шаг 3: изменение положения закрашенного блока

Мы будем использовать transform: translate3d(x, y, z) для изменения положения закрашенного блока прогресс бара.

Здесь необходимо обратить внимание на пару деталей. Во-первых нам необходимо менять только x координату, а остальные оставить 0. Во-вторых мы используем translate3d вместо translateX по соображениям производительности — чтобы позволить GPU обрабатывать его.

Напрямую в style указываем необходимые стили:

<div
class="filled-bar"
:style="{ transform: `translate3d(-${(1 - value) * 100}%, 0, 0)` }"
></div>

Теперь вы можете проверить что прогресс бар работает корректно, меняя дефолтное значение value. Поставьте например 0.5 и вы увидите это:

Шаг 4: подсчет текущего значения скролла пользователя

Чтобы получить это значение, мы должны сначала прослушать событие @scroll в .text-section, а затем разделить текущую позицию прокрутки (в пикселях) на полную высоту текстового блока.

В App.vue добавляем @scroll и ref к секции .text-section вот так:

<div
class="text-section"
ref="text"
@scroll="onScroll"
>

Нам необходимо добавить ref к элементу, что бы получить доступ к позиции скролла и его высоте.

Теперь напишем метод onScroll:

methods: {
onScroll () {
const progress = this.$refs.text.scrollTop / (this.$refs.text.scrollHeight - this.$refs.text.clientHeight)
if (progress > 1) {
this.progress = 1
} else if (progress < 0) {
this.progress = 0
} else {
this.progress = progress
}
}
}

Итак, когда пользователь проскроллит текст в блоке, мы обновляем значение прогресса в App.vue затем передаём его компоненту индикатора выполнения (через value), который затем обновит позицию заполненного блока и поменяет текст прогресса чтения.