Skip to content

Instantly share code, notes, and snippets.

@artemsites
Last active May 11, 2024 14:43
Show Gist options
  • Save artemsites/dc226e9ce8cdb25ce63a677304c0c39c to your computer and use it in GitHub Desktop.
Save artemsites/dc226e9ce8cdb25ce63a677304c0c39c to your computer and use it in GitHub Desktop.
<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 &#39;@/helpers/onEventEndThenStartCallback&#39;
</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>Например строку &quot;select=1&quot;</p>
<pre><code>:items=&quot;{
&#39;select=0&#39;: &#39;ВСЕ&#39;,
&#39;select=1&#39;: &#39;Опция номер 1&#39;,
&#39;select=2&#39;: &#39;Опция 2&#39;,
&#39;select=3&#39;: &#39;Длинная опция с&lt;br&gt;переносом строки&lt;br&gt;номер 3&#39;,
&#39;select=4&#39;: &#39;Опция 4&#39;
}&quot;
</code></pre>
<h2 id="3-как-подключается-компонент-в-vue-и-nuxt">3. Как подключается компонент в Vue и Nuxt</h2>
<pre><code>&lt;script setup&gt;
/* Только в Vue, не в Nuxt: */ import SelectDynamicBorder from &quot;@/components/SelectDynamicBorder.vue&quot;
function selectOnChange(selectedOption) {
console.log(&#39;selectedOption&#39;)
console.log(selectedOption)
}
&lt;/script&gt;
&lt;template&gt;
&lt;div&gt;
&lt;SelectDynamicBorder
:mobHorizontal=&quot;false&quot;
:pcHorizontal=&quot;true&quot;
@selectOnChange=&quot;selectOnChange&quot;
:items=&quot;{
&#39;select=0&#39;: &#39;ВСЕ&#39;,
&#39;select=1&#39;: &#39;Опция номер 1&#39;,
&#39;select=2&#39;: &#39;Опция 2&#39;,
&#39;select=3&#39;: &#39;Длинная опция с&lt;br&gt;переносом строки&lt;br&gt;номер 3&#39;,
&#39;select=4&#39;: &#39;Опция 4&#39;
}&quot;
:defaultOptionActive=&quot;0&quot;&gt;
&lt;/SelectDynamicBorder&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;SelectDynamicBorder
@selectOnChange=&quot;selectOnChange&quot;
class=&quot;filter__item&quot;
:mobHorizontal=&quot;true&quot;
:pcHorizontal=&quot;true&quot;
:icos=&quot;true&quot; :items=&quot;{
&#39;select=0&#39;: &#39;ВСЕ&#39;,
&#39;select=1&#39;: &#39;https://www.svgrepo.com/show/462828/vechain-circle.svg&#39;,
&#39;select=2&#39;: &#39;https://www.svgrepo.com/show/462828/vechain-circle.svg&#39;,
&#39;select=3&#39;: &#39;https://www.svgrepo.com/show/462828/vechain-circle.svg&#39;,
&#39;select=4&#39;: &#39;https://www.svgrepo.com/show/462828/vechain-circle.svg&#39;,
&#39;select=5&#39;: &#39;https://www.svgrepo.com/show/462828/vechain-circle.svg&#39;
}&quot;
:defaultOptionActive=&quot;0&quot;&gt;
&lt;/SelectDynamicBorder&gt;
&lt;/div&gt;
&lt;/template&gt;
</code></pre>

Динамический индикатор выбора варианта в виде рамки | VueJS / NuxtJS

1. Зависимости:

  • Функция onEventEndThenStartCallback.js
    Статья

Зависимость (функция) кладётся в папку (например) helpers и подключается в самом компоненте (это уже прописано в компоненте):

import { onEventEndThenStartCallback } from '@/helpers/onEventEndThenStartCallback'

2. Компонент возвращает ключ прописанный в items:

Например строку "select=1"

:items="{ 
  'select=0': 'ВСЕ', 
  'select=1': 'Опция номер 1', 
  'select=2': 'Опция 2', 
  'select=3': 'Длинная опция с<br>переносом строки<br>номер 3', 
  'select=4': 'Опция 4' 
}" 

3. Как подключается компонент в Vue и Nuxt

<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>

Сам код компонента "Динамического индикатора выбора варианта в виде рамки | VueJS / NuxtJS

[[code code="https://api.cacher.io/raw/5d2071529b7f9d516ebe/b86924b6c9aa0ca6980f/SelectDynamicBorder.vue"]]

<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>
<pre><code>&lt;script setup&gt;
// Только в Vue, не в Nuxt нужно импортировать:
// import { ref, onMounted } from &#39;vue&#39;
import { onEventEndThenStartCallback } from &#39;@/helpers/onEventEndThenStartCallback&#39;
let props = defineProps([&#39;items&#39;, &#39;defaultOptionActive&#39;, &#39;icos&#39;, &#39;mobHorizontal&#39;, &#39;pcHorizontal&#39;/* По умолчанию вертикальное расположение */])
let selectsWidth = []
let selectsHeight = []
let arSelectsOffsetLeft = []
let arSelectsOffsetTop = []
let widthIndicator = ref(&#39;100%&#39;)
let heightIndicator = ref(&#39;100%&#39;)
let offsetLeftIndicator = ref(&#39;0%&#39;)
let offsetTopIndicator = ref(&#39;0%&#39;)
let indexActive = ref(props.defaultOptionActive)
let selectRootNode = ref(null)
onMounted(() =&gt; {
let selectItemAll = selectRootNode.value.querySelectorAll(&#39;.select__item&#39;)
setSizes()
window.addEventListener(&quot;resize&quot;, 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] + &#39;px&#39;
offsetTopIndicator.value = arSelectsOffsetTop[indexActive.value] + &#39;px&#39;
widthIndicator.value = selectsWidth[indexActive.value] + &#39;px&#39;
heightIndicator.value = selectsHeight[indexActive.value] + &#39;px&#39;
}
})
function setWidthToIndicator(i) {
widthIndicator.value = selectsWidth[i] + &#39;px&#39;
heightIndicator.value = selectsHeight[i] + &#39;px&#39;
}
function setOffsetLeftIndicator(i) {
offsetLeftIndicator.value = arSelectsOffsetLeft[i]+&#39;px&#39;
}
function setOffsetTopIndicator(i) {
offsetTopIndicator.value = arSelectsOffsetTop[i]+&#39;px&#39;
}
function setOptionIndex(i) {
indexActive.value = i
}
&lt;/script&gt;
&lt;template&gt;
&lt;!-- @doc По умолчанию вертикальное расположение --&gt;
&lt;div
:class=&quot;[&#39;select&#39;, (mobHorizontal) ? &#39;--mob-horizontal&#39; : &#39;&#39;, (pcHorizontal) ? &#39;--pc-horizontal&#39; : &#39;&#39;, (icos) ? &#39;--icos&#39; : &#39;&#39;]&quot;
ref=&quot;selectRootNode&quot;&gt;
&lt;div
v-for=&quot;(name, value, i) in items&quot;
class=&quot;select__item&quot;
:key=&quot;i&quot;
@click=&quot;setOptionIndex(i); setWidthToIndicator(i); setOffsetLeftIndicator(i); setOffsetTopIndicator(i); $emit(&#39;selectOnChange&#39;, value);&quot;&gt;
&lt;!-- @doc элемент для выбора всех вариантов должен быть с ключом 0 ! --&gt;
&lt;template v-if=&quot;icos &amp;&amp; name!==&#39;ВСЕ&#39;&quot;&gt;
&lt;img :src=&quot;name&quot; alt=&quot;&quot; class=&quot;select__item-ico&quot;&gt;
&lt;/template&gt;
&lt;template
v-else-if=&quot;icos &amp;&amp; name === &#39;ВСЕ&#39;&quot;&gt;
&lt;div class=&quot;select__item-ico --all&quot; v-html=&quot;name&quot;&gt;&lt;/div&gt;
&lt;/template&gt;
&lt;div v-else v-html=&quot;name&quot; class=&quot;select__item-text&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div
class=&quot;select__indicator&quot;
:style=&quot;[`--index:${indexActive};`, `--width:${widthIndicator};`, `--height:${heightIndicator};`, `--left:${offsetLeftIndicator};`, `--top:${offsetTopIndicator};`]&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;style lang=&quot;scss&quot;&gt;
.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 {
&amp;__item {
margin: 0 $margin-right 0 0;
&amp;:last-of-type {
margin-right: 0;
}
}
&amp;__border {
transform: unset;
top: unset;
left: var(--left);
}
}
&amp;.--mob-horizontal {
@media screen and (max-width: 575.98px) {
justify-content: center;
flex-direction: row;
}
}
&amp;.--mob-horizontal &amp; {
@media screen and (max-width: 575.98px) {
@include style-horizontal;
}
}
&amp;.--pc-horizontal {
@media screen and (min-width: 576px) {
justify-content: center;
flex-direction: row;
}
}
&amp;.--pc-horizontal &amp; {
@media screen and (min-width: 576px) {
@include style-horizontal;
}
}
&amp;.--icos &amp; {
&amp;__item {
width: $ico-width;
height: $ico-height;
padding: 0.5rem;
&amp;-ico {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
&amp;.--all {
border: 1px solid black;
}
}
}
}
&amp;__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;
}
&amp;-ico {
width: 100%;
height: 100%;
}
&amp;-text {
br {
@media screen and (max-width: 575.98px) {
content: &#39;&#39;;
display: none;
}
}
}
}
&amp;__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;
}
}
}
&lt;/style&gt;
</code></pre>
<h2>Сам код компонента "Динамического индикатора выбора варианта в виде рамки | VueJS / NuxtJS</h2>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment