這是 30 個在 30 天內用 JavaScript 寫的網站練習,我也有紀錄平時寫的 HTML/CSS 練習作品在這個 Repo。關於這個 JavaScript30 的挑戰,我把完成後的心得與一些想法整理於Medium,歡迎與我交流 :)
transitionend
事件
querySelector 尋找 dataset attribute 符合
querySelector(`div[data-key="${e.keyCode}"]`)
div[data-key${matching}]
可以直接在 querySelector 找到特定 node<audio data-key="65" src="sounds/clap.wav"></audio>
// 從頭播放
audio.currentTime = 0;
audio.play();
transform-origin
CSS 屬性
transition-timing-function
CSS 屬性
transition-timing-function: cubic-bezier(0.1, 2.7, 0.58, 1);
CSS Varialbes
:root {
--spacing: 10px;
--blur: 10px;
--base: #8282e6;
}
img {
padding: var(--spacing);
background: var(--base);
filter: blur(var(--blur));
}
JS 變更 CSS Variables
<label for="blur">Blur:</label>
<input id="blur" type="range" name="blur" min="0" max="25" value="10" data-sizing="px">
<label for="base">Base Color</label>
<input id="base" type="color" name="base" value="#8282e6">
inputs.forEach((input) => input.addEventListener('change', updateCSS));
inputs.forEach((input) => input.addEventListener('mousemove', updateCSS));
function updateCSS() {
const suffix = this.dataset.sizing || '';
document.documentElement.style.setProperty(`--${this.name}`, this.value + suffix);
}
HTML <input> 參數
Filter
回傳符合條件的元素組成的陣列
const fifteen = inventors.filter(inventor => (inventor.year >= 1500 && inventor.year < 1600))
map
回傳透過函式內回傳的值組合成一個陣列
const fullName = inventors.map((inventor) => inventor.first + ' ' + inventor.last);
sort
回傳符合條件排序後的陣列
const ordered = inventors.sort((first, second) => first.year > second.year ? 1 : -1);
const sorted = inventors.sort((first, second) => {
const lastPerson = first.passed - first.year;
const nextPerson = second.passed - second.year;
return lastPerson > nextPerson ? -1 : 1;
});
const alpha = people.sort((a, b) => {
const [aLast, aFirst] = a.split(", ");
const [bLast, bFirst] = b.split(", ");
return aLast > bLast ? 1 : -1;
});
reduce
與前一個回傳的值再次作運算,詳細使用為:
array.reduce(reducer[accumlator, currentValue, currentIndex, array], initialValue)
const totalYears = inventors.reduce((total, inventor) => {
return total + (inventor.passed - inventor.year);
}, 0);
const data = ['car', 'car', 'truck', 'truck', 'bike', 'walk', 'car', 'van', 'bike', 'walk', 'car', 'van', 'car'];
const transport = data.reduce((obj, item) => {
if (!obj[item]) {
obj[item] = 0;
}
obj[item]++;
return obj;
}, {});
tips
display: flex
flex: flex-grow flex-shrink flex-basis
transition-timing-function
先縮後放效果
classList.toggle(className)
remove()
,無則 add()
transitionend
event
e.propertyName
抓到 transition 的事件e.propertyName
條件,可以把多個 transition 串起來if (e.propertyName.includes('flex'))
解決fetch()
XMLHttpRequest
的方案jQuery.ajax()
不同點在於,當接收到一個代表錯誤的 HTTP 狀態碼時,從fetch()
返回的 Promise 不會被標記為 reject
, 即使該 HTTP 的狀態碼是 404 或 500。相反,它會將 Promise 狀態標記為 resolve (但是會將 resolve 的返回值的 ok 屬性設置為 false ),僅當網絡故障時或請求被阻止時,才會標記為 rejectfetch()
的處理可以用 .then()
串接,會得到 response
fetch(url)
.then((blob) => blob.json())
.then((data) => cities.push(...data));
.json()
是 response 的 method.push(...data)
即時監聽 <input> 有無變化需要同時監聽兩個事件
change
keyup
把 array 裡的物件轉成 HTML 的方法
for loop
map + return + .join('')
function displayMatches() {
const matchArray = findMatches(this.value, cities);
const html = matchArray
.map((place) => {
return `
<li>
<span class="name">${cityName}, ${stateName}</span>
<span class="population">${numberWithCommas(place.population)}</span>
</li>
`;
})
.join('');
suggestions.innerHTML = html;
}
.join('')
是為了把大陣列轉成一個字串
RegExp(wordToMatch, 'gi')
.match(regex)
返回符合的值.replace(regex, replacingWord)
返回替代後的值為數字加分隔號
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
例子(把 Array 變成 Html,把其中相符的值變色)
function displayMatches() {
const matchArray = findMatches(this.value, cities);
const html = matchArray
.map((place) => {
const regex = new RegExp(this.value, 'gi');
const cityName = place.city.replace(regex, `<span class="hl">${this.value}</span>`);
const stateName = place.state.replace(regex, `<span class="hl">${this.value}</span>`);
return `
<li>
<span class="name">${cityName}, ${stateName}</span>
<span class="population">${numberWithCommas(place.population)}</span>
</li>
`;
})
.join('');
suggestions.innerHTML = html;
}
some
檢查陣列中元素,有一元素符合條件則回傳 true
const isAdult = people.some((person) => new Date().getFullYear() - person.year >= 19);
every
檢查陣列中元素,全部元素符合條件則回傳 true
const allAdults = people.every((person) => new Date().getFullYear() - person.year >= 19);
find
回傳陣列中第一個符合條件的元素
const comment = comments.find((comment) => comment.id === 823423);
findIndex
回傳陣列中第一個符合條件的元素索引
const index = comments.findIndex((comment) => comment.id === 823423);
// comments.splice(index, 1);
const newComments = [...comments.slice(0, index), ...comments.slice(index + 1)];
splice vs slice
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
array.slice([begin[, end]])
slice
組成新陣列,則可用const newComments = [...comments.slice(0, index), ...comments.slice(index + 1)];
JS 取得現在視窗大小
window.innerWidth
, window.innerHeight
Canvas 設置
const canvas = document.querySelector('#draw');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Start drawing
ctx.beginPath();
// start from
ctx.moveTo(lastX, lastY);
// go to
ctx.lineTo(e.offsetX, e.offsetY);
// Draw
ctx.stroke();
Array deconstruct 技巧
[X, Y] = [newX, newY];
hsl 顏色
hsl(hue, saturation, lightness)
Chrome dev tools
console.log()
%s
=> 加入字串console.log('Hello I am a %s string!', '💩');
%c
=> 加入 CSSconsole.log('%c I am some great text', 'font-size:50px; background:red; text-shadow: 10px 10px 0 blue');
console 系列
console.warn()
console.error()
console.info()
console.assert(statement, 'Word that show when statement == false')
console.assert(p.classList.contains('ouch'), 'That is wrong!');
console.clear()
console.dir()
console.log(p);
console.dir(p);
console.group()
/ console.groupCollapsed()
+ console.log()
*n + console.groupEnd()
dogs.forEach((dog) => {
console.groupCollapsed(`${dog.name}`);
console.log(`This is ${dog.name}`);
console.log(`${dog.name} is ${dog.age} years old`);
console.log(`${dog.name} is ${dog.age * 7} dog years old`);
console.groupEnd(`${dog.name}`);
});
console.count()
console.time()
+ console.timeEnd()
console.time('fetching data');
fetch('https://api.github.com/users/wesbos')
.then((data) => data.json())
.then((data) => {
console.timeEnd('fetching data');
console.log(data);
});
console.table()
偵測使用者用 shift 鍵做選取
e.shiftKey
<input>[type=”checkbox”]
用 input:checked+指定元素
去操作打勾後的 CSS 變化
input:checked+p {
background: #F9F9F9;
text-decoration: line-through;
}
用 !isBoolean 操作 toggle
if (node === lastChecked || node === this) {
isInBetween = !isInBetween;
}
<video> html tag
autoplay
<video> node 操作
video.paused
video.currentTime
video.duration
video.play()
video.pause()
video.addEventListener('play'/'pause'/'timeupdate');
querySelector
可以將 node 當作目標選取內元素
const player = document.querySelector('.player');
const video = player.querySelector('.viewer');
querySelector
可以將 attribute 當作 selector
const skipButtons = player.querySelectorAll('[data-skip]');
將物件 method 當作變數執行
const method = video.paused ? 'play' : 'pause';
video[method]();
改變 node 內文字正統方法
toggle.textContent = icon;
<input> range 改變屬性的簡潔寫法
HTML
<input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1">
<input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1">
JS
function handleRangeUpdate() {
video[this.name] = this.value;
}
flex 調整比例做進度條
display: flex
flex: >0
flex-basis: 100%
flex: 0
flex-basis: progress percentage
JS 選取元素長度
e.offsetX
node.offsetWidth
if statement 則執行一個 function 的簡潔寫法
(e) => mousedown && scrub(e);
addEventListener('keyup', (e)=>{console.log(e.key)})
.splice()
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
debounce
計算 Scroll 高度 scrollY
+ innerHeight
window.scrollY
:視窗上緣離網頁上緣的距離window.innerHeight
:視窗目前的高度計算網頁到元素最上緣的距離
const slideInAt = window.scrollY + window.innerHeight - sliderImage.height;
計算網頁到元素最下緣的距離
const imageBottom = sliderImage.offsetTop + sliderImage.height;
node.offsetTop
copy 一個陣列的四種方法
const team2 = players.slice();
const team3 = [].concat(players);
const team4 = [...players];
const team5 = Array.from(players);
copt 一個物件的三種方法
const cap2 = Object.assign({}, person, { number: 99, age: 12 });
const cap3 = { ...person };
const dev2 = JSON.parse(JSON.stringify(wes));
Note
JSON.parse(JSON.stringify(wes))
這個方法會遍歷每一層的物件,其他方法都只能 copy 一層<form> tag
form.addEventListener('submit')
會吃到 enter
、click
等等const text = this.querySelector('[name=item]').value;
this.reset()
可以把 input 清空<label> tag
id
=> for
<input type="checkbox" data-index=${i} id="item${i}"/>
<label for="item${i}">${plate.text}</label>
input:checked + label:before
控制變化.plates input {
display: none;
}
.plates input + label:before {
content: '⬜️';
margin-right: 10px;
}
.plates input:checked + label:before {
content: '🌮';
}
Local Storage
localStorage.setItems('key', 'value');
localStorage.getItem('key');
localStorage.remove('key');
toString()
,所以設置前要先把 object 轉成 stringlocalStorage.setItem('items', JSON.stringify(items));
Delegation
e.target.matches('yourTarget')
指定array.map()
可編輯文字的 tag attribute
contenteditable
destructor
const { offsetWidth: width, offsetHeight: height } = hero;
let { offsetX: x, offsetY: y } = e;
JS 中的四捨五入
math.round()
CSS textShadow
:可以同時給多個值
${xShadow}px ${yShadow}px ${blur}px ${color}
RegExp
對照前綴有無 a the then
return bandName.replace(/^(a |the |an )/i, '').trim();
注意對照空格 (a |the )
跟(a|the)
還有 (a| the |)
結果不同
轉換陣列元素型態到數值
array.map(parseFloat);
轉換 NodeList 到 Array
// Array.from
const timeNodes = Array.from(document.querySelectorAll('[data]'));
// Spread
const timeNodes = [...document.querySelectorAll('[data]')];
無條件捨去
Math.floor()
Demo steps:
cd 19\ -\ Webcam\ Fun/
npm install
npm run start
取得 Webcam 權限
server
/ localhost
package.json
{
"name": "gum",
"version": "1.0.0",
"description": "",
"main": "scripts.js",
"scripts": {
"start": "browser-sync start --server --files \"*.css, *.html, *.js\""
},
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.12.5 <2.23.2"
}
}
JS 中取得 Webcam 影像
navigator.mediaDevices.getUserMedia
會得到一個 Promise 物件video.src = window.URL.createObjectURL(localMediaStream);
拿到影像navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((localMediaStream) => {
console.log(localMediaStream);
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
})
.catch((err) => {
console.error(`OH NO!!!`, err);
});
拿到 video 的實際寬高
video.videoHieght
, video.videoWidth
用 canvas 輸出 Webcame Stream
return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
}, 16);
監聽 video 準備好的事件
video.addEventListener('canplay', paintToCanvas);
把 canvas 資料取出,轉化成 Base64
const data = canvas.toDataURL('image/jpeg');
const link = document.createElement('a');
link.href = data;
Base64 資料
設定可下載的連結跟預覽
link.setAttribute('download', 'handsome');
link.innerHTML = <img src="${data}" alt="Handsome Man" />;
取得 canvas 中影像的 pixel
let pixels = ctx.getImageData(0, 0, width, height);
更改 pixel 產生 filter
pixel[0]
到 pixel[3]
分別代表 rgbafunction redEffect(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0] = pixels.data[i + 0] + 200; // RED
pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREEN
pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Blue
}
return pixels;
}
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i - 150] = pixels.data[i + 0]; // RED
pixels.data[i + 500] = pixels.data[i + 1]; // GREEN
pixels.data[i - 550] = pixels.data[i + 2]; // Blue
}
return pixels;
}
ctx.globalAlpha = 0.1;
把更改後的 pixel 放回 canvas
ctx.putImageData(pixels, 0, 0);
prepend child 的方法
outer.insertBefore(inner, outer.firsChild);
debugger
Demo steps:
cd 20\ -\ Speech\ Detection/
npm install
npm run start
瀏覽器中的 Speech Recognition
window.SpeechRecognition
or window.webkitSpeechRecognition
基本設置
const recognition = new SpeechRecognition();
// ? 即時辨識 : 停頓辨識
recognition.interimResults = true;
recognition.lang = 'en-US';
recognition.start();
監聽 recognition 事件
recognition.addEventListener('result')
recognition.addEventListener('end')
result 回傳事件
e.results
=> 回傳一個 SpeechRecognitionResultList
e.results[0].isFinal
=> 回傳布林值判斷是否有斷句e.results[0].transript
=> 回傳辨識結果Demo steps:
cd 21\ -\ Geolocation/
npm install
npm run start
navigator.geolocation.watchPosition(data)
data.coords.speed
data.coords.heading
this.getBoundingClientRect()
translate
的話,要加上 scroll
的數值把 text 轉換成 voice
speechSynthesis
負責接收文字轉換發出聲音new SpeechSynthesisUtterance()
負責設定文字素材監聽 speechSynthesis
事件
speechSynthesis.addEventListener('voiceschanged', populateVoices);
speechSynthesis
method
.getVoice()
得到發出聲音的人 .name
和語言縮寫 .lang
.speak()
發出聲音.cancel()
終止發聲從 dropdown 選單找對應 property
msg.voice = voices.find((voice) => voice.name === this.value);
在 addEventListener 中的 callback 加入參數的方法
addEventListener('event', toggle.bind(null, this));
addEventListener('event', () => toggle(false));
重複利用同個 function 做 speak 跟 stop(類似多型概念)
function togglePlay(startOver = true) {
speechSynthesis.cancel();
if (startOver) {
speechSynthesis.speak(msg);
}
}
speakButton.addEventListener('click', togglePlay);
stopButton.addEventListener('click', () => togglePlay(false));
padding-top
= offsetHeight
抵銷e.propagation()
capture: true
捕獲階段觸發once: ture
只觸發一次後就 unbind 事件從 display: none
& opacity: 0
出現的效果
display: block
,設 setTimeout 讓 opacity: 1
在 150ms 後在變換setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);
指定 hover 到的元素下的元素
const dropdown = this.querySelector('.dropdown');
const cords = dropdown.getBoundingClientRect();
Drag and scroll 效果,需要監聽的事件
mousedown
, mouseleave
, mouseup
, mousemove
Click 在外層元素裡的位置
e.pageX
在整個網頁的位置- slider.offsetLeft
扣掉外層元素的位置console.log debug 小技巧
製造 scroll 效果
// mousedown
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
// mousemove
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3;
slider.scrollLeft = scrollLeft - walk;
在外層元素裡的位置高度
e.pageY - this.offsetTop
元素 height 用 percent 衡量
const height = Math.round(percent * 100) + '%';
小數點後兩位
number.toFixed(2)
video 播放速度
video.playbackRate
setInterval
累加顯示時間的問題
setInterval
Date.now()
new Date( Date.now() )
中得到 Date
物件倒數計時
const now = Date.now();
const then = now + seconds * 1000;
setInterval(() => {
const secLeft = Math.round((then - Date.now()) / 1000);
}, 1000);
終止 setInterval
setInterval
指派給一個變數 var1clearInterval(var1)
setInterval
不會在第零秒時觸發,故開始時間要用額外 operation
網頁的標題 tab
document.title
用 name attribute 取代 querySelector
const customForm = document.customForm;
Form & Input
submit
監聽e.preventDefault()
避免重新整理this.reset()
清空 inputRandom(min, max)
return Math.round(Math.random() * (max - min) + min);
避免 fake mouse click
e.isTrusted = true