iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
Modern Web

30天30個前端任務系列 第 8

#8 Button Ripple Effect(原生JS版)、#5. Q&A Section(Vue版)

Button Ripple Effect(原生JS版)

CodePen Link: https://codepen.io/zyrxdkoz/pen/yLXgxOp

效果說明
滑鼠移到button點擊,會出現水波紋的效果。

實作邏輯

CSS

// 基本樣式
button {
  background-color: purple;
  color: #fff;
  border: 1px purple solid;
  border-radius: 30px;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 2px;
  padding: 20px 30px;
  overflow: hidden;
  margin: 10px 0;
  position: relative;
}

button:focus {
  outline: none;
}

// 預先寫好circle選擇器的樣式,準備讓javascript來生成
button .circle {
  position: absolute;
  background-color: #fff;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(0);
  animation: scale 0.5s ease-out;
}

// 動態效果設定
@keyframes scale {
  to {
    transform: translate(-50%, -50%) scale(3);
    opacity: 0;
  }
}

javascript

const buttons = document.querySelectorAll('.ripple')

buttons.forEach(button => {
    button.addEventListener('click', function (e) {
        // 選定滑鼠在視窗上的位置
        const x = e.clientX
        const y = e.clientY
        
        // 選定按鈕的頂部和側邊的位置
        const buttonTop = e.target.offsetTop
        const buttonLeft = e.target.offsetLeft
        
        // 根據出滑鼠點擊的地方和按鈕頂部、側邊,來判斷滑鼠到邊緣的距離。
        const xInside = x - buttonLeft
        const yInside = y - buttonTop
        
        // 建立一個<span>,然後加入circle選擇器做渲染
        const circle = document.createElement('span')
        circle.classList.add('circle')
        
        // 依照前面計算出來的兩個點,使渲染的circle擴展到邊緣
        circle.style.top = yInside + 'px'
        circle.style.left = xInside + 'px'
        
        // 將建立好的DOM element放到button當中
        this.appendChild(circle)
        
        // 效果維持0.5秒就讓它消失
        setTimeout(() => circle.remove(), 500)
    })
})

Q&A Section(Vue版)

Demo Link| Git Commit

in SectionQA.vue

// src/views/SectionQA.vue

<template>
	<home-btn />

	<div
		class="
			faq-container
			w-full
			h-[vh100]
			bg-gray-100
			p-5
			flex flex-col
			justify-start
			items-center
		"
	>
		<h1
			class="
				text-md text-gray-900
				font-extrabold
				mt-10
				mb-10
				md:text-2xl
				lg:text-3xl
			"
		>
			Frequently Asked Questions
		</h1>
        // 用v-for迴圈渲染模板,然後每個渲染出來的QA都會綁定qa.id和toggleAcitve
		<div v-for="qa in qaArray" :key="qa.id" @click="toggleActive(qa.id)">
			<div
				class="
					faq
					w-[50vw]
					relative
					overflow-hidden
					bg-transparent
					border-2 border-gray-500
					rounded-xl
					mb-8
					p-7
					duration-150
					ease-in-out
				"
                // 若qa.isActive為true,則帶入active選擇器
				:class="{ active: qa.isActive }"
			>
				<h3 class="faq-title font-semibold mr-[50px]">
					{{ qa.question }}
				</h3>
				<p class="faq-answer text-red-800 mt-5">{{ qa.answer }}</p>
				<button
					class="
						faq-toggle
						flex
						group
						justify-center
						items-center
						absolute
						p-0
						top-3
						right-3
						rounded-full
						h-[30px]
						w-[30px]
						border-gray-500
					"
				>
					<Icon
						icon="bx:bxs-chevron-down"
						class="icon-arrow-down text-gray-800 pointer-events-none"
						height="36"
					/>
					<Icon
						icon="pepicons:times"
						class="icon-times text-[#111] pointer-events-none"
						height="28"
					/>
				</button>
			</div>
		</div>
	</div>
</template>
<script>
import { Icon } from '@iconify/vue'
import HomeBtn from '../components/HomeBtn.vue'
import { ref } from 'vue'

export default {
	components: { HomeBtn, Icon },
	setup() {
        // 建立好資料,然後運用v-for迴圈來渲染到模板上
		const data = [
			{
				question: "Why shouldn't we trust atoms?",
				answer: 'They make up everything',
				isActive: true,
				id: 1,
			},
			{
				question: 'What do you call someone with no body and no nose?',
				answer: 'Nobody Knows',
				isActive: false,
				id: 2,
			},
			{
				question: "What's the object-oriented way to become wealthy?",
				answer: 'Inheritance',
				isActive: false,
				id: 3,
			},
			{
				question: 'How many tickles does it take to tickle an octopus?',
				answer: 'Ten-tickles!',
				isActive: false,
				id: 4,
			},
			{
				question: 'What is: 1 + 1?',
				answer: 'Depends on who are you asking.',
				isActive: false,
				id: 5,
			},
		]
		const qaArray = ref(data)
        
        // 當QA被點擊的時候,會帶入id比對,然後改變或維持isActive的值。
		const toggleActive = (id) => {
			qaArray.value.forEach(function (qa) {
				if (id === qa.id) {
					qa.isActive = !qa.isActive
				} else {
					qa.isActive
				}
			})
		}
        // 將被ref函式包裝進去的陣列,以及toggleActive函式渲染到模板中
		return {
			qaArray,
			toggleActive,
		}
	},
}
</script>

// css部分主要是彌補tailwind css達成的效果。
<style lang="scss" scoped>
.faq {
	transition: 0.3s ease;
	&.active {
		background-color: white;
		.faq-answer {
			display: block;
		}
		.faq-toggle {
			background-color: #9fa4a8;
			.icon-arrow-down {
				display: none;
			}
			.icon-times {
				display: block;
				color: white;
			}
			&:focus {
				outline: none;
			}
		}
	}
	.faq-title {
		margin: 0 35px 0 0;
	}
	.faq-answer {
		display: none;
	}
	.icon-times {
		display: none;
	}
}
</style>

實作過程的心得

  1. 實作邏輯和Expanding Cards很相似,都是用v-for渲染資料,然後用forEach逐一比對id,來判斷是否要加入active選擇器。但這次實作挑戰看看能否完全靠tailwind css達成效果。
  2. 承1,目前答案是不行 XD。最接近的成果請參考tailwind playground,其中有運用到group、group-focus、focus-within的屬性。就算能夠做到,也比較難達到toggle的狀態。(截稿前突然想到應該可以用label for的方式,會再嘗試看看)

明日任務

  1. Netflix Navigation(原生JS版)

上一篇
#6. Scroll Animation(原生JS版), #7. Progress Steps(原生JS版)
下一篇
#9. Netflix Sidebar(原生JS版)
系列文
30天30個前端任務19

尚未有邦友留言

立即登入留言