跳至主要内容

[JS] 常用 JavaScript snippets

UI 相關

將捲軸到該元素最下方 (scroll to bottom)

const scrollToBottom = (targetElement) => {
targetElement.scrollTo({
top: targetElement.scrollHeight,
behavior: 'smooth',
});
};

避免按鈕點選後卡在 focus 狀態

keywords: prevent button focus when click

使用 e.currentTarget.blur()

<button id="prevent-focus">FooBar</button>
const btn = document.querySelector('#prevent-focus');
btn.addEventListener('click', (e) => e.currentTarget.blur());

如果只使用 e.target.blur() 有可能當點選按鈕中的圖案時,因為 target 指稱到的是裡面的圖案,所以會無效。

取得鼠標座標(需檢查有效性)

function mousePosition(event) {
if (event.pageX || event.pageY) {
return { x: event.pageX, y: event.pageY };
}
return {
x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
y: event.clientY + document.body.scrollTop - document.body.clientTop,
};
}

取得座標相關資料(需檢查有效性)

檔案可視區域寬: document.documentElement.clientWidth
檔案可視區域高: document.documentElement.clientHeight

網頁可見區域寬: document.body.clientWidth
網頁可見區域高: document.body.clientHeight
網頁可見區域寬: document.body.offsetWidth (包括邊線的寬)
網頁可見區域高: document.body.offsetHeight (包括邊線的高)
網頁正文全文寬: document.body.scrollWidth
網頁正文全文高: document.body.scrollHeight
網頁被捲去的高: document.body.scrollTop
網頁被捲去的左: document.body.scrollLeft
網頁正文部分上: window.screenTop
網頁正文部分左: window.screenLeft
螢幕分辨率的高: window.screen.height
螢幕分辨率的寬: window.screen.width
螢幕可用工作區高度: window.screen.availHeight
螢幕可用工作區寬度: window.screen.availWidth

取得瀏覽器捲軸位置

let scrollOffset = window.scrollY || document.documentElement.scrollTop;

window.scrollY @ MDN

Lazy Load

相關內容可參考:Preload Content 內容預先載入(Lazy Load)

font lazy load

可以監聽字體載入的事件,當字體載入完成後,再放入 CSS 的 font-family 內,如此可以避免一開始畫面沒有文字的情況

// font lazy load
document.fonts.load('12px Noto Sans TC').then(function () {
const body = document.querySelector('body');
body.style.fontFamily = `"Noto Sans TC", ${window
.getComputedStyle(body)
.getPropertyValue('font-family')}`;
});

RGB To Hex

const RGBToHex = (r, g, b) => ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0');

RGB to Hex @ 30 seconds of code

Random Hex Color

const randomHexColorCode = () => {
let n = (Math.random() * 0xfffff * 1000000).toString(16);
return '#' + n.slice(0, 6);
};

Random Hex Color @ 30 seconds of code

格式化上傳檔案大小

const prettyBytes = (num, precision = 3, addSpace = true) => {
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if (Math.abs(num) < 1) return num + (addSpace ? ' ' : '') + UNITS[0];
const exponent = Math.min(Math.floor(Math.log10(num < 0 ? -num : num) / 3), UNITS.length - 1);
const n = Number(((num < 0 ? -num : num) / 1000 ** exponent).toPrecision(precision));
return (num < 0 ? '-' : '') + n + (addSpace ? ' ' : '') + UNITS[exponent];
};

prettyBytes(1000); // "1 KB"
prettyBytes(-27145424323.5821, 5); // "-27.145 GB"
prettyBytes(123456789, 3, false); // "123MB"

Pretty Bytes @ 30 seconds of code

限制上傳檔案類型

<!-- 當今瀏覽器 -->
<input type="file" name="filePath" accept=".jpg,.jpeg,.doc,.docx,.pdf" />

<!-- 限制圖片 -->
<input type="file" class="file" value="上傳" accept="image/*" />

好用工具(Utilities)

pause / sleep

const pause = (ms) => {
let time = new Date();
while (new Date() - time <= ms);
};

text truncate

// DEMO:
// https://repl.it/@PJCHENder/TextTruncate-Function

var truncate = function (elem, limit, after) {
// Make sure an element and number of items to truncate is provided
if (!elem || !limit) return;

// Get the inner content of the element
var content = elem.textContent.trim();

// Convert the content into an array of words
// Remove any words above the limit
content = content.split(' ').slice(0, limit);

// Convert the array of words back into a string
// If there's content to add after it, add it
content = content.join(' ') + (after ? after : '');

// Inject the content back into the DOM
elem.textContent = content;
};

How to truncate text with vanilla JavaScript

debounce

throttledebounce 的差別在於,throttle 指的是在「一定時間」內最多只能促發某函式「多少次」;debounce 則是指在「一定時間」內能夠促發這個函式「一次」

/**
* 將要 debounce 的函式代入 func 中,不用 invoke
* wait,設定時間(毫秒)
* immediate 如果是 true 則在事件觸發時會立即執行,
* 但事件結束時不會執行;如果是 false 則事件觸發時不會立即執行,
* 但事件結束時會執行。
**/

export function debounce(callback, wait, immediate) {
let timeout;
function wrapper(...args) {
const self = this;
const later = () => {
timeout = null;
if (!immediate) callback.apply(self, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) callback.apply(this, args);
}

return wrapper;
}

The Difference between Throttling and Debouncing @ CSS Tricks Debouncing and Throttling Explained Through Examples @ CSS Tricks

在一般的情況下使用

/**
* 一秒內只會觸發一次 click 內的 callback
**/
const btn = document.querySelector('#btn');
btn.addEventListener(
'click',
debounce(
(e) => {
console.log('click', e);
},
1000,
true,
),
);

在 Vue 中使用

// 將 debounce function 放在 Vue Instance 外面
function debounce () { ... }

new Vue({
methods: {
wantThisDebounce: debounce(function(){
// Put what you want to do here
}, 1500, false)
}
})

JavaScript debounce function @ DWB

throttle

export function throttle(callback, limit) {
let wait = false;
// DONOT change to arrow function, otherwise 'this' can not be reset
function wrapper(...args) {
if (!wait) {
callback.apply(this, args);
wait = true;
setTimeout(() => {
wait = false;
}, limit);
}
}
return wrapper;
}

debounce and throttle example

HTML
<button id="debounce">debounce</button> <button id="throttle">throttle</button>
JS
const debounceBtn = document.querySelector('#debounce');
const throttleBtn = document.querySelector('#throttle');

class Pet {
constructor(animal) {
this.animal = animal;
}

meow() {
console.log(`${this.animal} is meow`);
}
}

const dog = new Pet('dog');

debounceBtn.addEventListener('click', debounce(log('debounce'), 1000, false));
throttleBtn.addEventListener('click', throttle(log('debounce'), 1000));

/* debounceBtn.addEventListener("click", debounce(dog.meow, 1000, false).bind(dog));
throttleBtn.addEventListener("click", throttle(dog.meow, 1000).bind(dog));
*/
let count = 0;
function log(message) {
return (e) => {
console.log('message', message);
e.preventDefault();
console.log(count++);
};
}

function throttle(callback, limit) {
let wait = false;

function wrapper(...args) {
if (!wait) {
callback.apply(this, args);
wait = true;
setTimeout(() => {
wait = false;
}, limit);
}
}

return wrapper;
}

function debounce(callback, wait, immediate) {
let timeout;
function wrapper(...args) {
const self = this;
const later = function () {
timeout = null;
if (!immediate) {
callback.apply(self, args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
callback.apply(this, args);
}
}
return wrapper;
}

camel case, snake case 的轉換

export const camelize = (text) => {
return text.replace(/^([A-Z])|[\s-_]+(\w)/g, function (match, p1, p2) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};

export const decamelize = (str, separator) => {
separator = typeof separator === 'undefined' ? '_' : separator;

return str
.replace(/([a-z\d])([A-Z])/g, '$1' + separator + '$2')
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + separator + '$2')
.replace(/([\d]+)/g, separator + '$1')
.toLowerCase();
};

export const snakeToCamelCase = (o) => {
if (typeof o === 'string') {
return camelize(o);
} else if (typeof o === 'object') {
return Object.fromEntries(
Object.entries(o).map(([key, value]) => {
if (value && typeof value === 'object') {
return [camelize(key), snakeToCamelCase(value)];
}
return [camelize(key), value];
}),
);
}
};

export const camelCaseToSnake = (o) => {
if (typeof o === 'string') {
return decamelize(o);
} else if (typeof o === 'object') {
return Object.fromEntries(
Object.entries(o).map(([key, value]) => {
if (value && typeof value === 'object') {
return [decamelize(key), camelCaseToSnake(value)];
}
return [decamelize(key), value];
}),
);
}
};

型別判斷

typeof true; // 'boolean'
typeof 'foobar'; // 'string'
typeof 123; // 'number'
typeof NaN; // 'number'
typeof {}; // 'object'
typeof []; // 'object'
typeof undefined; // 'undefined'

typeof window.alert; // 'function'
typeof null; // 'object'

判斷式否為陣列

let arr = [];
Array.isArray(arr);

判斷是否為函式(function)

if (typeof v === 'function') {
// do something
}

Check if a variable is of function type @ StackOverflow

判斷型別與值是否相同(equality comparisons)

js-equality

時間相關

時間倒數計時

<p id="left-time"></p>
function countdown() {
var endTime = new Date('May 2, 2018 21:31:09');
var nowTime = new Date();

if (nowTime >= endTime) {
document.getElementById('left-time').innerHTML = '倒計時間結束';
return;
}

var leftSecond = parseInt((endTime.getTime() - nowTime.getTime()) / 1000);
if (leftSecond < 0) {
leftSecond = 0;
}

days = parseInt(leftSecond / 3600 / 24);
hours = parseInt((leftSecond / 3600) % 24);
minutes = parseInt((leftSecond / 60) % 60);
seconds = parseInt(leftSecond % 60);

document.getElementById('left-time').innerHTML =
days + '天' + hours + '小時' + minutes + '分' + seconds + '秒';
}

countdown();

setInterval(countdown, 1000);

時間戳記格式化

function formatDate(now) {
var y = now.getFullYear();
var m = now.getMonth() + 1; // 注意 JavaScript 月份+1
var d = now.getDate();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();

return y + '-' + m + '-' + d + ' ' + h + ':' + m + ':' + s;
}

var nowDate = new Date(1442978789184);

alert(formatDate(nowDate));

檢查瀏覽器是否支援 SVG

function isSupportSVG() {
var SVG_NS = 'http://www.w3.org/2000/svg';
return !!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect;
}

console.log(isSupportSVG());

檢查瀏覽器是否支援 canvas

function isSupportCanvas() {
return document.createElement('canvas').getContext ? true : false;
}

console.log(isSupportCanvas());

檢測是否移動端及瀏覽器內核

var browser = {
versions: function () {
var u = navigator.userAgent;
return {
trident: u.indexOf('Trident') > -1, //IE內核
presto: u.indexOf('Presto') > -1, //opera內核
webKit: u.indexOf('AppleWebKit') > -1, //蘋果、谷歌內核
gecko: u.indexOf('Firefox') > -1, //火狐內核Gecko
mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否移動終端
ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios
android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, //android
iPhone: u.indexOf('iPhone') > -1, //iPhone
iPad: u.indexOf('iPad') > -1, //iPad
webApp: u.indexOf('Safari') > -1, //Safari
};
},
};

if (
browser.versions.mobile() ||
browser.versions.ios() ||
browser.versions.android() ||
browser.versions.iPhone() ||
browser.versions.iPad()
) {
alert('移動端');
}

檢測是否電腦端/移動端

var browser = {
versions: (function () {
var u = navigator.userAgent,
app = navigator.appVersion;
var sUserAgent = navigator.userAgent;
return {
trident: u.indexOf('Trident') > -1,
presto: u.indexOf('Presto') > -1,
isChrome: u.indexOf('chrome') > -1,
isSafari: !u.indexOf('chrome') > -1 && /webkit|khtml/.test(u),
isSafari3: !u.indexOf('chrome') > -1 && /webkit|khtml/.test(u) && u.indexOf('webkit/5') != -1,
webKit: u.indexOf('AppleWebKit') > -1,
gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,
mobile: !!u.match(/AppleWebKit.*Mobile.*/),
ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1,
iPhone: u.indexOf('iPhone') > -1,
iPad: u.indexOf('iPad') > -1,
iWinPhone: u.indexOf('Windows Phone') > -1,
};
})(),
};

if (browser.versions.mobile || browser.versions.iWinPhone) {
window.location = 'http:/www.baidu.com/m/';
}

檢查瀏覽器內核

function getInternet() {
if (navigator.userAgent.indexOf('MSIE') > 0) {
return 'MSIE'; //IE瀏覽器
}

if ((isFirefox = navigator.userAgent.indexOf('Firefox') > 0)) {
return 'Firefox'; //Firefox瀏覽器
}

if ((isSafari = navigator.userAgent.indexOf('Safari') > 0)) {
return 'Safari'; //Safan瀏覽器
}

if ((isCamino = navigator.userAgent.indexOf('Camino') > 0)) {
return 'Camino'; //Camino瀏覽器
}
if ((isMozilla = navigator.userAgent.indexOf('Gecko/') > 0)) {
return 'Gecko'; //Gecko瀏覽器
}
}

強制手機橫向顯示

$(window).on('orientationchange', function (event) {
if (event.orientation == 'portrait') {
$('body').css('transform', 'rotate(90deg)');
} else {
$('body').css('transform', 'rotate(0deg)');
}
});
$(window).orientationchange();

參考