Last active
May 11, 2024 14:43
-
-
Save artemsites/dc226e9ce8cdb25ce63a677304c0c39c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h1 id="динамический-индикатор-выбора-варианта-в-виде-рамки--vuejs--nuxtjs">Динамический индикатор выбора варианта в виде рамки | VueJS / NuxtJS</h1> | |
<h2 id="1-зависимости">1. Зависимости:</h2> | |
<ul> | |
<li><strong>Функция onEventEndThenStartCallback.js</strong><br><a target="_blank" href="https://practical-web.ru/javascript/kak-vypolnit-callback-po-zaversheniyu-sobytiya-na-javascript" title="Открыть в новой вкладке">Статья</a></li> | |
</ul> | |
<p>Зависимость (функция) кладётся в папку (например) helpers и подключается в самом компоненте (это уже прописано в компоненте):</p> | |
<pre><code>import { onEventEndThenStartCallback } from '@/helpers/onEventEndThenStartCallback' | |
</code></pre> | |
<p><img src="https://artemsites.ru/storage/components/select-dynamic-border/select-dynamic-border.gif" alt=""></p> | |
<h2 id="2-компонент-возвращает-ключ-прописанный-в-items">2. Компонент возвращает ключ прописанный в items:</h2> | |
<p>Например строку "select=1"</p> | |
<pre><code>:items="{ | |
'select=0': 'ВСЕ', | |
'select=1': 'Опция номер 1', | |
'select=2': 'Опция 2', | |
'select=3': 'Длинная опция с<br>переносом строки<br>номер 3', | |
'select=4': 'Опция 4' | |
}" | |
</code></pre> | |
<h2 id="3-как-подключается-компонент-в-vue-и-nuxt">3. Как подключается компонент в Vue и Nuxt</h2> | |
<pre><code><script setup> | |
/* Только в Vue, не в Nuxt: */ import SelectDynamicBorder from "@/components/SelectDynamicBorder.vue" | |
function selectOnChange(selectedOption) { | |
console.log('selectedOption') | |
console.log(selectedOption) | |
} | |
</script> | |
<template> | |
<div> | |
<SelectDynamicBorder | |
:mobHorizontal="false" | |
:pcHorizontal="true" | |
@selectOnChange="selectOnChange" | |
:items="{ | |
'select=0': 'ВСЕ', | |
'select=1': 'Опция номер 1', | |
'select=2': 'Опция 2', | |
'select=3': 'Длинная опция с<br>переносом строки<br>номер 3', | |
'select=4': 'Опция 4' | |
}" | |
:defaultOptionActive="0"> | |
</SelectDynamicBorder> | |
<br> | |
<br> | |
<br> | |
<SelectDynamicBorder | |
@selectOnChange="selectOnChange" | |
class="filter__item" | |
:mobHorizontal="true" | |
:pcHorizontal="true" | |
:icos="true" :items="{ | |
'select=0': 'ВСЕ', | |
'select=1': 'https://www.svgrepo.com/show/462828/vechain-circle.svg', | |
'select=2': 'https://www.svgrepo.com/show/462828/vechain-circle.svg', | |
'select=3': 'https://www.svgrepo.com/show/462828/vechain-circle.svg', | |
'select=4': 'https://www.svgrepo.com/show/462828/vechain-circle.svg', | |
'select=5': 'https://www.svgrepo.com/show/462828/vechain-circle.svg' | |
}" | |
:defaultOptionActive="0"> | |
</SelectDynamicBorder> | |
</div> | |
</template> | |
</code></pre> |
- Функция onEventEndThenStartCallback.js
Статья
Зависимость (функция) кладётся в папку (например) helpers и подключается в самом компоненте (это уже прописано в компоненте):
import { onEventEndThenStartCallback } from '@/helpers/onEventEndThenStartCallback'
Например строку "select=1"
:items="{
'select=0': 'ВСЕ',
'select=1': 'Опция номер 1',
'select=2': 'Опция 2',
'select=3': 'Длинная опция с<br>переносом строки<br>номер 3',
'select=4': 'Опция 4'
}"
<script setup>
/* Только в Vue, не в Nuxt: */ import SelectDynamicBorder from "@/components/SelectDynamicBorder.vue"
function selectOnChange(selectedOption) {
console.log('selectedOption')
console.log(selectedOption)
}
</script>
<template>
<div>
<SelectDynamicBorder
:mobHorizontal="false"
:pcHorizontal="true"
@selectOnChange="selectOnChange"
:items="{
'select=0': 'ВСЕ',
'select=1': 'Опция номер 1',
'select=2': 'Опция 2',
'select=3': 'Длинная опция с<br>переносом строки<br>номер 3',
'select=4': 'Опция 4'
}"
:defaultOptionActive="0">
</SelectDynamicBorder>
<br>
<br>
<br>
<SelectDynamicBorder
@selectOnChange="selectOnChange"
class="filter__item"
:mobHorizontal="true"
:pcHorizontal="true"
:icos="true" :items="{
'select=0': 'ВСЕ',
'select=1': 'https://www.svgrepo.com/show/462828/vechain-circle.svg',
'select=2': 'https://www.svgrepo.com/show/462828/vechain-circle.svg',
'select=3': 'https://www.svgrepo.com/show/462828/vechain-circle.svg',
'select=4': 'https://www.svgrepo.com/show/462828/vechain-circle.svg',
'select=5': 'https://www.svgrepo.com/show/462828/vechain-circle.svg'
}"
:defaultOptionActive="0">
</SelectDynamicBorder>
</div>
</template>
[[code code="https://api.cacher.io/raw/5d2071529b7f9d516ebe/b86924b6c9aa0ca6980f/SelectDynamicBorder.vue"]]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script setup> | |
// Только в Vue, не в Nuxt нужно импортировать: | |
// import { ref, onMounted } from 'vue' | |
import { onEventEndThenStartCallback } from '@/helpers/onEventEndThenStartCallback' | |
let props = defineProps(['items', 'defaultOptionActive', 'icos', 'mobHorizontal', 'pcHorizontal'/* По умолчанию вертикальное расположение */]) | |
let selectsWidth = [] | |
let selectsHeight = [] | |
let arSelectsOffsetLeft = [] | |
let arSelectsOffsetTop = [] | |
let widthIndicator = ref('100%') | |
let heightIndicator = ref('100%') | |
let offsetLeftIndicator = ref('0%') | |
let offsetTopIndicator = ref('0%') | |
let indexActive = ref(props.defaultOptionActive) | |
let selectRootNode = ref(null) | |
onMounted(() => { | |
let selectItemAll = selectRootNode.value.querySelectorAll('.select__item') | |
setSizes() | |
window.addEventListener("resize", onEventEndThenStartCallback(750, setSizes)) | |
function setSizes() { | |
selectsWidth=[] | |
selectsHeight=[] | |
arSelectsOffsetLeft=[] | |
arSelectsOffsetTop=[] | |
for (let selectItem of selectItemAll) { | |
selectsWidth.push(selectItem.getBoundingClientRect().width.toFixed(0)) | |
selectsHeight.push(selectItem.getBoundingClientRect().height.toFixed(0)) | |
arSelectsOffsetLeft.push(selectItem.offsetLeft) | |
arSelectsOffsetTop.push(selectItem.offsetTop) | |
} | |
offsetLeftIndicator.value = arSelectsOffsetLeft[indexActive.value] + 'px' | |
offsetTopIndicator.value = arSelectsOffsetTop[indexActive.value] + 'px' | |
widthIndicator.value = selectsWidth[indexActive.value] + 'px' | |
heightIndicator.value = selectsHeight[indexActive.value] + 'px' | |
} | |
}) | |
function setWidthToIndicator(i) { | |
widthIndicator.value = selectsWidth[i] + 'px' | |
heightIndicator.value = selectsHeight[i] + 'px' | |
} | |
function setOffsetLeftIndicator(i) { | |
offsetLeftIndicator.value = arSelectsOffsetLeft[i]+'px' | |
} | |
function setOffsetTopIndicator(i) { | |
offsetTopIndicator.value = arSelectsOffsetTop[i]+'px' | |
} | |
function setOptionIndex(i) { | |
indexActive.value = i | |
} | |
</script> | |
<template> | |
<!-- @doc По умолчанию вертикальное расположение --> | |
<div | |
:class="['select', (mobHorizontal) ? '--mob-horizontal' : '', (pcHorizontal) ? '--pc-horizontal' : '', (icos) ? '--icos' : '']" | |
ref="selectRootNode"> | |
<div | |
v-for="(name, value, i) in items" | |
class="select__item" | |
:key="i" | |
@click="setOptionIndex(i); setWidthToIndicator(i); setOffsetLeftIndicator(i); setOffsetTopIndicator(i); $emit('selectOnChange', value);"> | |
<!-- @doc элемент для выбора всех вариантов должен быть с ключом 0 ! --> | |
<template v-if="icos && name!=='ВСЕ'"> | |
<img :src="name" alt="" class="select__item-ico"> | |
</template> | |
<template | |
v-else-if="icos && name === 'ВСЕ'"> | |
<div class="select__item-ico --all" v-html="name"></div> | |
</template> | |
<div v-else v-html="name" class="select__item-text"></div> | |
</div> | |
<div | |
class="select__indicator" | |
:style="[`--index:${indexActive};`, `--width:${widthIndicator};`, `--height:${heightIndicator};`, `--left:${offsetLeftIndicator};`, `--top:${offsetTopIndicator};`]"> | |
</div> | |
</div> | |
</template> | |
<style lang="scss"> | |
.select { | |
$color: red; | |
$border-width: 0.2rem; | |
$margin-bottom: 0.2rem; | |
$margin-right: 0.4rem; | |
$ico-width: 4.5rem; | |
$ico-height: 4.5rem; | |
@media screen and (min-width: 576px) { | |
$ico-width: 4rem; | |
$ico-height: 4rem; | |
} | |
display: inline-flex; | |
align-items: center; | |
flex-direction: column; | |
position: relative; | |
@mixin style-horizontal { | |
&__item { | |
margin: 0 $margin-right 0 0; | |
&:last-of-type { | |
margin-right: 0; | |
} | |
} | |
&__border { | |
transform: unset; | |
top: unset; | |
left: var(--left); | |
} | |
} | |
&.--mob-horizontal { | |
@media screen and (max-width: 575.98px) { | |
justify-content: center; | |
flex-direction: row; | |
} | |
} | |
&.--mob-horizontal & { | |
@media screen and (max-width: 575.98px) { | |
@include style-horizontal; | |
} | |
} | |
&.--pc-horizontal { | |
@media screen and (min-width: 576px) { | |
justify-content: center; | |
flex-direction: row; | |
} | |
} | |
&.--pc-horizontal & { | |
@media screen and (min-width: 576px) { | |
@include style-horizontal; | |
} | |
} | |
&.--icos & { | |
&__item { | |
width: $ico-width; | |
height: $ico-height; | |
padding: 0.5rem; | |
&-ico { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 0.3rem; | |
&.--all { | |
border: 1px solid black; | |
} | |
} | |
} | |
} | |
&__item { | |
width: max-content; | |
height: max-content; | |
display: inline-flex; | |
justify-content: center; | |
align-items: center; | |
text-align: center; | |
padding: 0.3rem 0.8rem; | |
font-size: 1.2rem; | |
cursor: pointer; | |
margin: 0 auto $margin-bottom auto; | |
position: relative; | |
z-index: 1; | |
@media screen and (min-width: 576px) { | |
font-size: 1rem; | |
} | |
&-ico { | |
width: 100%; | |
height: 100%; | |
} | |
&-text { | |
br { | |
@media screen and (max-width: 575.98px) { | |
content: ''; | |
display: none; | |
} | |
} | |
} | |
} | |
&__indicator { | |
width: var(--width); | |
height: var(--height); | |
border-radius: 0.4rem; | |
border: $border-width solid $color; | |
box-sizing: content-box; | |
transition: 500ms; | |
transform: translate(-$border-width, -$border-width); | |
position: absolute; | |
left: var(--left); | |
top: var(--top); | |
z-index: 0; | |
@media screen and (min-width: 576px) { | |
border-width: 0.15rem; | |
} | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<pre><code><script setup> | |
// Только в Vue, не в Nuxt нужно импортировать: | |
// import { ref, onMounted } from 'vue' | |
import { onEventEndThenStartCallback } from '@/helpers/onEventEndThenStartCallback' | |
let props = defineProps(['items', 'defaultOptionActive', 'icos', 'mobHorizontal', 'pcHorizontal'/* По умолчанию вертикальное расположение */]) | |
let selectsWidth = [] | |
let selectsHeight = [] | |
let arSelectsOffsetLeft = [] | |
let arSelectsOffsetTop = [] | |
let widthIndicator = ref('100%') | |
let heightIndicator = ref('100%') | |
let offsetLeftIndicator = ref('0%') | |
let offsetTopIndicator = ref('0%') | |
let indexActive = ref(props.defaultOptionActive) | |
let selectRootNode = ref(null) | |
onMounted(() => { | |
let selectItemAll = selectRootNode.value.querySelectorAll('.select__item') | |
setSizes() | |
window.addEventListener("resize", onEventEndThenStartCallback(750, setSizes)) | |
function setSizes() { | |
selectsWidth=[] | |
selectsHeight=[] | |
arSelectsOffsetLeft=[] | |
arSelectsOffsetTop=[] | |
for (let selectItem of selectItemAll) { | |
selectsWidth.push(selectItem.getBoundingClientRect().width.toFixed(0)) | |
selectsHeight.push(selectItem.getBoundingClientRect().height.toFixed(0)) | |
arSelectsOffsetLeft.push(selectItem.offsetLeft) | |
arSelectsOffsetTop.push(selectItem.offsetTop) | |
} | |
offsetLeftIndicator.value = arSelectsOffsetLeft[indexActive.value] + 'px' | |
offsetTopIndicator.value = arSelectsOffsetTop[indexActive.value] + 'px' | |
widthIndicator.value = selectsWidth[indexActive.value] + 'px' | |
heightIndicator.value = selectsHeight[indexActive.value] + 'px' | |
} | |
}) | |
function setWidthToIndicator(i) { | |
widthIndicator.value = selectsWidth[i] + 'px' | |
heightIndicator.value = selectsHeight[i] + 'px' | |
} | |
function setOffsetLeftIndicator(i) { | |
offsetLeftIndicator.value = arSelectsOffsetLeft[i]+'px' | |
} | |
function setOffsetTopIndicator(i) { | |
offsetTopIndicator.value = arSelectsOffsetTop[i]+'px' | |
} | |
function setOptionIndex(i) { | |
indexActive.value = i | |
} | |
</script> | |
<template> | |
<!-- @doc По умолчанию вертикальное расположение --> | |
<div | |
:class="['select', (mobHorizontal) ? '--mob-horizontal' : '', (pcHorizontal) ? '--pc-horizontal' : '', (icos) ? '--icos' : '']" | |
ref="selectRootNode"> | |
<div | |
v-for="(name, value, i) in items" | |
class="select__item" | |
:key="i" | |
@click="setOptionIndex(i); setWidthToIndicator(i); setOffsetLeftIndicator(i); setOffsetTopIndicator(i); $emit('selectOnChange', value);"> | |
<!-- @doc элемент для выбора всех вариантов должен быть с ключом 0 ! --> | |
<template v-if="icos && name!=='ВСЕ'"> | |
<img :src="name" alt="" class="select__item-ico"> | |
</template> | |
<template | |
v-else-if="icos && name === 'ВСЕ'"> | |
<div class="select__item-ico --all" v-html="name"></div> | |
</template> | |
<div v-else v-html="name" class="select__item-text"></div> | |
</div> | |
<div | |
class="select__indicator" | |
:style="[`--index:${indexActive};`, `--width:${widthIndicator};`, `--height:${heightIndicator};`, `--left:${offsetLeftIndicator};`, `--top:${offsetTopIndicator};`]"> | |
</div> | |
</div> | |
</template> | |
<style lang="scss"> | |
.select { | |
$color: red; | |
$border-width: 0.2rem; | |
$margin-bottom: 0.2rem; | |
$margin-right: 0.4rem; | |
$ico-width: 4.5rem; | |
$ico-height: 4.5rem; | |
@media screen and (min-width: 576px) { | |
$ico-width: 4rem; | |
$ico-height: 4rem; | |
} | |
display: inline-flex; | |
align-items: center; | |
flex-direction: column; | |
position: relative; | |
@mixin style-horizontal { | |
&__item { | |
margin: 0 $margin-right 0 0; | |
&:last-of-type { | |
margin-right: 0; | |
} | |
} | |
&__border { | |
transform: unset; | |
top: unset; | |
left: var(--left); | |
} | |
} | |
&.--mob-horizontal { | |
@media screen and (max-width: 575.98px) { | |
justify-content: center; | |
flex-direction: row; | |
} | |
} | |
&.--mob-horizontal & { | |
@media screen and (max-width: 575.98px) { | |
@include style-horizontal; | |
} | |
} | |
&.--pc-horizontal { | |
@media screen and (min-width: 576px) { | |
justify-content: center; | |
flex-direction: row; | |
} | |
} | |
&.--pc-horizontal & { | |
@media screen and (min-width: 576px) { | |
@include style-horizontal; | |
} | |
} | |
&.--icos & { | |
&__item { | |
width: $ico-width; | |
height: $ico-height; | |
padding: 0.5rem; | |
&-ico { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 0.3rem; | |
&.--all { | |
border: 1px solid black; | |
} | |
} | |
} | |
} | |
&__item { | |
width: max-content; | |
height: max-content; | |
display: inline-flex; | |
justify-content: center; | |
align-items: center; | |
text-align: center; | |
padding: 0.3rem 0.8rem; | |
font-size: 1.2rem; | |
cursor: pointer; | |
margin: 0 auto $margin-bottom auto; | |
position: relative; | |
z-index: 1; | |
@media screen and (min-width: 576px) { | |
font-size: 1rem; | |
} | |
&-ico { | |
width: 100%; | |
height: 100%; | |
} | |
&-text { | |
br { | |
@media screen and (max-width: 575.98px) { | |
content: ''; | |
display: none; | |
} | |
} | |
} | |
} | |
&__indicator { | |
width: var(--width); | |
height: var(--height); | |
border-radius: 0.4rem; | |
border: $border-width solid $color; | |
box-sizing: content-box; | |
transition: 500ms; | |
transform: translate(-$border-width, -$border-width); | |
position: absolute; | |
left: var(--left); | |
top: var(--top); | |
z-index: 0; | |
@media screen and (min-width: 576px) { | |
border-width: 0.15rem; | |
} | |
} | |
} | |
</style> | |
</code></pre> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h2>Сам код компонента "Динамического индикатора выбора варианта в виде рамки | VueJS / NuxtJS</h2> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment