Last active
October 25, 2023 10:44
-
-
Save CarsonSlovoka/bea02a1bf61f3113c9f2053fef59470b to your computer and use it in GitHub Desktop.
carousel component
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
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 沒有補上用調整大小或者設定成其他手機尺寸的時候,效果會不如預期 --> | |
<style> | |
/* 外層屬性可以對slot有影響,如果元件裡面有定義該屬性也是會被直接取代(以外層定義的為主),即便用了important也是一樣 */ | |
.item { | |
color: red; | |
} | |
</style> | |
<body> | |
<h2>範例一</h2> | |
<carson-carousel> | |
<div slot="header">特價商品</div> | |
<!-- <div class="carousel-item">商品1</div> 因為這些內容將來都會被放到shadowRoot之中,所以css不需要怕被蓋掉,可以用簡短的名稱 --> | |
<div class="item" > | |
<a href="https://zh.wikipedia.org/wiki/%E8%88%92%E8%B7%91"> | |
<img style="height:50px" src="https://encrypted-tbn0.gstatic.com/shopping?q=tbn:ANd9GcSUa3O9uEvf6Y7asgczmVgmFqkdGaNOsQXbCIdt9sTd7Bz0Y0CW9yVzYSzY6c5ERSye1rtWv-pectB1C_1gECKByhr5yuG7_3Y3bt2O-5Qcj4Cc4YRVgrNA9GVseCLgucXaIn9WnwFbFQ&usqp=CAc"> | |
舒跑 | |
</a> | |
</div> | |
<div class="item">商品2</div> | |
<div class="item"> | |
<img width="75" src="https://media.discordapp.net/attachments/468414088850964480/1110938867458134016/vimgopher.png?ex=654261f2&is=652fecf2&hm=3de0a5b638e9954ead24e2d65099b83a8bbb1b00bc78e45aba036eaf2a56b021&="/> | |
商品3 | |
</div> | |
<div class="item">商品4</div> | |
<div class="item">商品5</div> | |
</carson-carousel> | |
<h2>範例二: 設定寬度、高度</h2> | |
<carson-carousel style="width: 100%; height:200px"> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
</carson-carousel> | |
</body> | |
<h2>範例三: 無商品測試</h2> | |
<carson-carousel></carson-carousel> | |
<h2>範例4: hover item的背景突顯</h2> | |
<carson-carousel style="--item-hover-bg-color: #adad0e"> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
</carson-carousel> | |
<h2>範例5: max-display:4 min-display:3</h2> | |
<carson-carousel max-display=4 min-display=3> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
<div class="item">商品4</div> | |
<div class="item">商品5</div> | |
</carson-carousel> | |
<h2>範例6: no-jump 無下方跳轉節點</h2> | |
<carson-carousel no-jump> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
<div class="item">商品4</div> | |
<div class="item">商品5</div> | |
</carson-carousel> | |
<h2>範例6: interval 自動跳轉 每3秒一個</h2> | |
<carson-carousel interval="3000"> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
<div class="item">商品4</div> | |
<div class="item">商品5</div> | |
</carson-carousel> | |
<h2>範例6: interval 自動跳轉 預設3秒,用javascript改成0.5秒</h2> | |
<carson-carousel id="rotate-test" interval="3000"> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
</carson-carousel> | |
<h2>範例6: 投放單張廣告</h2> | |
<carson-carousel max-display=1 min-display="1" interval="3000"> | |
<div class="item">商品1</div> | |
<div class="item">商品2</div> | |
<div class="item">商品3</div> | |
</carson-carousel> | |
</body> | |
</body> | |
<script> | |
class CarouselComponent extends HTMLElement { | |
#defaultMaxDisplay = 2 // 預設顯示3個項目,下標值從0開始 | |
#minDisplay = 1 // 預設顯示2個,下標值從0開始。在畫面寬度太小的時候(768),會調整顯示的項目 | |
#intervalID = null // 自動跳轉的interval取消用 | |
#curIndex = 0 // 當前跳轉到哪一個項目 | |
constructor() { | |
super() | |
this.attachShadow({mode: 'open'}) // 使用 Shadow DOM (這個可以達到隔離的效果,也就是裡面所定義的CSS,不會影響到外層) | |
this.jumpAble = this.getAttribute("no-jump") === null | |
if (Number(this.getAttribute("min-display")) !== 0) { | |
this.#minDisplay = Number(this.getAttribute("min-display")) - 1 | |
} | |
this.maxDisplay = Number(this.getAttribute("max-display")) | |
if (this.maxDisplay === 0) { | |
this.maxDisplay = this.#defaultMaxDisplay // 下標值為0 | |
} else { | |
this.maxDisplay -= 1 // 下標值從0開始 | |
this.#defaultMaxDisplay = this.maxDisplay | |
} | |
this.mediaQueryMaxWidthLE768 = window.matchMedia('(max-width: 768px)') // @media (max-width: 768px) 最大寬度在768以內時 | |
} | |
connectedCallback() { | |
this.render() | |
this.addEventListeners() | |
this.mediaQueryMaxWidthLE768.addEventListener("change", e => { | |
if (e.matches) { | |
this.maxDisplay = this.#minDisplay | |
} else { | |
this.maxDisplay = this.#defaultMaxDisplay // 預設設定的大小 | |
} | |
// 重新調整需要顯示的項目 | |
this.updateDisplay() | |
}) | |
} | |
render() { | |
this.shadowRoot.innerHTML = ` | |
<style> | |
.carousel-container { | |
position: relative; | |
display: flex; | |
align-items: center; | |
/* background-image: linear-gradient(45deg, rgba(231, 49, 97, 1) 10%, rgba(236, 107, 34, 1), rgb(182, 163, 70)); 太亮 */ | |
/* background-image: linear-gradient(#0005, #0008), url("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcShoB9hrCeKz6-5edIAjsk5QNhM7r5qfCoqhA&usqp=CAU"); */ | |
background-image: linear-gradient(#70707055, #8e8b8b88), /* 前面補上一層暗化背景圖片,避免太過突兀 */ | |
linear-gradient(45deg, rgba(231, 49, 97, 1) 10%, rgba(236, 107, 34, 1), rgb(182, 163, 70)); | |
/* width: var(--width, max-content); */ | |
flex-direction: column; | |
} | |
.carousel-body { | |
display: flex; | |
align-items: center; | |
} | |
.carousel-items { | |
display: flex; | |
overflow: hidden; | |
/* width: 100%; 所有商品的寬度 */ | |
width: max-content; | |
} | |
header { | |
color: white; | |
font-weight: 800; | |
} | |
.item { | |
width: 100px; | |
height: 100px; | |
margin: 10px; | |
color: white; | |
border: 1px solid black; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
flex-direction: column; /* 讓商品描述可以在圖片下 */ | |
} | |
.item:hover { | |
background-color: var(--item-hover-bg-color, initial); | |
color: var(--item-hover-color, white); | |
} | |
.item img { | |
max-width: -webkit-fill-available; | |
} | |
.item a { | |
display: contents; /* 背景、邊框、間距等都將消失,不佔用多餘的位置 */ | |
} | |
.item a:visited { | |
color: white; | |
} | |
.carousel-control { | |
position: sticky; | |
width: 40px; | |
height: 40px; | |
/* background-color: rgba(0, 0, 0, 0.5); */ | |
cursor: pointer; | |
transition: background-color 0.3s; | |
} | |
.carousel-control.prev { | |
left: 0; | |
} | |
.carousel-control.next { | |
right: 0; | |
} | |
.carousel-control:hover { | |
background-color: rgba(0, 0, 0, 0.6); | |
--alpha: 1; | |
} | |
.carousel-control.prev::before, | |
.carousel-control.next::before { | |
content: ''; | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
border-width: 3px 3px 0 0; | |
border-style: solid; | |
width: 12px; | |
height: 12px; | |
} | |
.carousel-control.prev::before { | |
transform: translate(-25%, -50%) rotate(-135deg); /* 影響內文的位置 */ | |
/* border-color: #e5e3e3; 覆蓋之後的箭頭顏色 */ | |
border-color: rgba(229, 227, 227, var(--alpha, 0.1)); /* 一開始讓他不明顯,等hover之後再去等調整alpha數值 */ | |
} | |
.carousel-control.next::before { | |
transform: translate(-75%, -50%) rotate(45deg); | |
border-color: rgba(229, 227, 227, var(--alpha, 0.1)); | |
} | |
.jump-container { | |
margin-bottom: 0.5em; | |
} | |
.jump-container button { | |
background-color: rgba(255,255,255,0.5); | |
border: none; | |
border-radius: 2em; | |
width: 1em; | |
height: 1em; | |
margin-left: 1em; | |
cursor: pointer; | |
} | |
.jump-container button:hover { | |
background-color: rgba(255,255,255,1); | |
} | |
.jump-container button.active { | |
background-color: yellow; | |
width: 3em; | |
} | |
</style> | |
<div class="carousel-container"> | |
<header><slot name="header"></slot></header> | |
<div class="carousel-body"> | |
<div class="carousel-control prev" id="prevBtn"></div> | |
<div class="carousel-items"> | |
<slot></slot> | |
<!-- | |
<div class="carousel-item">商品1</div> | |
<div class="carousel-item">商品2</div> | |
<div class="carousel-item">商品3</div> | |
--> | |
</div> | |
<div class="carousel-control next" id="nextBtn"></div> | |
</div> | |
<div class="jump-container"></div> | |
</div> | |
` | |
this.shadowRoot.querySelector(".carousel-container").style = this.getAttribute("style") | |
this.shadowRoot.querySelector(".carousel-container").style.width = this.style.width === "" ? "max-content" : this.style.width | |
this.shadowRoot.querySelector(".carousel-container").class = this.getAttribute("class") | |
// 以下將slot的內容給抓到shadowRoot之中 | |
const itemsContainer = this.shadowRoot.querySelector('.carousel-items') | |
// const items = this.shadowRoot.querySelectorAll('.carousel-item') // 會找不到,因為這已經被slot取代 | |
// const items = itemsContainer.querySelector('slot').querySelectorAll('.carousel-item') // 不能直接找,需要透過assignedNodes | |
// 使用 assignedNodes() 獲取插槽的分配的節點,並過濾出元素節點 | |
// const items = itemsContainer.querySelector('slot').assignedNodes().filter(node => { | |
// if (node.nodeType === Node.ELEMENT_NODE) { | |
// return node.classList.contains("carousel-item") | |
// } | |
// return false | |
// }) | |
// 以上可以被替換成下面寫法 | |
const items = itemsContainer.querySelector('slot').assignedNodes().filter(node => | |
node.nodeType === Node.ELEMENT_NODE && node.classList.contains("item") | |
) | |
if (items) { | |
items.forEach((e, i) => { | |
e.dataset.order = `${i}` // data-order 紀錄一開始使用者的每一個商品的順序(此順序為固定,不被左右箭頭等作用被影響) | |
itemsContainer.append(e) | |
}) | |
} | |
itemsContainer.querySelectorAll('.item').forEach((e, i) => { | |
// 預設顯示調整 | |
if (i > this.maxDisplay) { | |
e.style.display = "none" | |
} | |
// 以下處理跳轉節點的內容 | |
if (this.jumpAble) { | |
const frag = document.createRange().createContextualFragment(`<button aria-label="${e.textContent}"></button>`) | |
frag.querySelector("button").onclick = (e) => { | |
this.jumpTo(i) | |
} | |
this.shadowRoot.querySelector('.jump-container').append(frag) | |
this.jumpTo(0) // 為了激活預設項目 | |
// 啟用autoRotate | |
this.autoRotate(Number(this.getAttribute("interval"))) | |
} | |
}) | |
} | |
addEventListeners() { | |
this.shadowRoot.getElementById('prevBtn').addEventListener('click', () => this.move(-1)) | |
this.shadowRoot.getElementById('nextBtn').addEventListener('click', () => this.move(1)) | |
} | |
move(direction) { | |
const itemsContainer = this.shadowRoot.querySelector('.carousel-items') | |
// 因為我們前面已經有把slot的內容,給抓附加到shadowRoot之中,所以這邊可以抓到內容 | |
const items = this.shadowRoot.querySelectorAll('.item') | |
if (items.length > 0) { | |
if (direction === -1) { | |
itemsContainer.append(items[0]); | |
} else if (direction === 1) { | |
itemsContainer.insertBefore(items[items.length - 1], items[0]) | |
} | |
this.updateDisplay() | |
// 更新跳轉項目 (第一個項目就是要被跳轉到的項目) | |
this.#curIndex = Number(this.shadowRoot.querySelector('.carousel-items .item').dataset.order) | |
this.jumpTo(this.#curIndex) | |
} | |
} | |
updateDisplay() { | |
this.shadowRoot.querySelector('.carousel-items').querySelectorAll('.item').forEach((e, i) => { | |
if (i > this.maxDisplay) { | |
e.style.display = "none" | |
} else { | |
e.style.display = "" | |
} | |
}) | |
} | |
jumpTo(index) { | |
if (!this.jumpAble) { | |
return | |
} | |
const itemsContainer = this.shadowRoot.querySelector('.carousel-items') | |
const items = [...this.shadowRoot.querySelectorAll('.item')].sort( | |
(a, b) => Number(a.dataset.order) - Number(b.dataset.order) | |
) | |
// 先全部篩除 | |
while (itemsContainer.firstChild) { | |
itemsContainer.removeChild(itemsContainer.firstChild) | |
} | |
// 附加該index開始到結束的節點 | |
for (let i = index; i < items.length; i++) { | |
itemsContainer.append(items[i]) | |
} | |
// 把剩餘的項目往後放 | |
for (let i = 0; i < index; i++) { | |
itemsContainer.append(items[i]) | |
} | |
this.updateDisplay() | |
// 處理active | |
this.shadowRoot.querySelectorAll(`.jump-container button`).forEach((elem, i) => { | |
i !== index ? elem.classList.remove("active") : // 非目前項目就把激活全部取消 | |
elem.classList.add("active") // 激活目前的項目 | |
}) | |
} | |
autoRotate(interval) { | |
if (interval === 0) { | |
return | |
} | |
const items = this.shadowRoot.querySelectorAll('.carousel-items .item') | |
// 使的這個函數可以被重複執行 | |
if (this.#intervalID) { | |
clearInterval(this.#intervalID) | |
} | |
this.#intervalID = setInterval(() => { | |
this.#curIndex++ | |
if (this.#curIndex >= items.length) { | |
this.#curIndex = 0 | |
} | |
this.jumpTo(this.#curIndex) | |
}, interval) | |
} | |
// 允許創建之後還可以修改跳轉的頻率 | |
updateInterval(newInterval) { | |
this.autoRotate(newInterval) | |
} | |
} | |
customElements.define('carson-carousel', CarouselComponent) | |
</script> | |
<script> | |
const carousel = document.querySelector("#rotate-test") | |
carousel.updateInterval(500) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
result