Last active
October 31, 2023 08:21
-
-
Save kawaz/804ddbcc7ddc5033d09d4e72fdcd3bf5 to your computer and use it in GitHub Desktop.
よく使うJSの便利関数的な奴 tools.js
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
// タイムアウト付き setInterval | |
const setIntervalTimeout = (f, interval = 200, timeout = 5000, immediate = false) => { | |
immediate && setTimeout(f, 0) | |
const i = setInterval(f, interval) | |
const t = setTimeout(() => clearInterval(i), timeout) | |
return () => { clearInterval(i); clearTimeout(t) } | |
} | |
// タイムアウト付き setInterval の強化版(タブがバックグラウンドになってもサボらない) | |
const setIntervalTimeoutSuper = (f, interval = 200, timeout = 5000, immediate = false) => { | |
try { | |
const code = `addEventListener('message',()=>{${immediate}&&setTimeout(postMessage,0,null);setInterval(postMessage,${interval},null);setTimeout(close,${timeout})})` | |
const w = new Worker(`data:text/javascript;base64,${btoa(code)}`) | |
w.onmessage = f | |
w.postMessage(null) | |
return ()=>w.terminate() | |
} catch(_){ | |
// 実装の問題またはCSPによる拒否などで Worker が使えなければ普通の setInterval を使う | |
return setIntervalTimeout(f, interval, immediate) | |
} | |
} | |
// f が truthy な値を返すまで繰り返し実行する。タイムアウトするとrejectされる。 | |
const waitValue = (f, {interval=100, timeout=3000, immediate=true}={}) => new Promise(async(ok,ng)=>{ | |
if(immediate){const r=await f();if(r)return ok(r)} | |
const i = setInterval(async(r)=>{if(r=await f())s(r)}, interval) | |
const t = setTimeout(s, timeout) | |
function s(r){clearTimeout(t);clearInterval(i);(r?ok:ng)(r)} | |
}) | |
// querySelector のショートハンド | |
const query = (target, mapFn=v=>v) => mapFn(target instanceof Element ? target : typeof target === 'string' ? document.querySelector(target) : null) | |
const queryAll = (target, mapFn=v=>v) => (target instanceof Element ? [target] : typeof target === 'string' ? [...document.querySelectorAll(target)] : []).map(mapFn) | |
// shadowRoot を貫通して検索できるバージョン | |
// TODO: selector が子孫セレクタ(空白区切り)を含むケースに未対応(優先度=高) | |
// TODO: selector が複数セレクタ(カンマを含む)ケースに未対応(優先度=低) | |
const queryAllDeep = (selector, rootDocOrMapFn=document, mapFn=v=>v) => { | |
// rootDoc は省略可能とする | |
if(typeof rootDocOrMapFn === 'function') { | |
mapFn = rootDoc | |
rootDocOrMapFn = document | |
} | |
const docs = (node=rootDocOrMapFn, doc=rootDocOrMapFn) => ({node, doc, shadows: [...doc.querySelectorAll("*")].filter(n=>n.shadowRoot).map(n=>docs(n,n.shadowRoot))}) | |
const allDocs = (d=docs())=>[d.doc, ...d.shadows.flatMap(s=>allDocs(s))] | |
return allDocs().flatMap(doc=>[...doc.querySelectorAll(selector)]) | |
} | |
const myStyle = (id) => myStyle[`$${id}`] ?? (myStyle[`$${id}`] = (document.head.append(document.createElement('style')), document.styleSheets.item(document.styleSheets.length - 1))) | |
const newRule = () => (s => (s.addRule(), s.cssRules[s.cssRules.length - 1]))(myStyle()) | |
// target が見つかるまで待つPromise | |
const ready = (target, interval = 200, timeout = 5000) => new Promise((resolve, reject) => { | |
const waiter = () => { | |
const el = query(target) | |
if (el) { | |
clearInterval(waiterId) | |
clearTimeout(timeoutId) | |
resolve(el) | |
} | |
} | |
const timeoutId = setTimeout(() => { clearInterval(waiterId); reject(new Error(`target ${target} was not ready`)); }, timeout) | |
const waiterId = setInterval(waiter, interval) | |
waiter() | |
}) | |
//イベントが最後に発生してからdelay経過後に1度だけ実行されるようにする(キーボードイベントとかに使える) | |
const debounce = (f, delay=500) => { | |
let t; | |
return (...args) => { | |
clearTimeout(t) | |
t = setTimeout(()=>f(...args), delay) | |
} | |
} | |
//指定の時間間隔毎に最大一度だけ実行されるようにする(スクロールやリサイズなど継続的に発生するイベントに使える) | |
const throttle = (f, limit=500)=>{ | |
let inThrottle = false; | |
return (...args) => { | |
if (!inThrottle) { | |
inThrottle = true | |
f(...args) | |
setTimeout(()=>inThrottle=false, limit) | |
} | |
}; | |
} | |
// 配列がソート済みかチェックする | |
const isSorted = (arr, cmp=(a,b)=>a===b?0:a<b?1:-1) => arr.every((cur,i,a)=>i==0||0<=cmp(a[i-1],cur)) | |
// DOMを作る | |
const createElement = (tag, props = {}, children = []) => { | |
const {classList=[], style={}, dataset={}, ...others} = props | |
const el = document.createElement(tag) | |
if(classList.length) { | |
el.classList.add(...classList) | |
} | |
Object.assign(el.style, style) | |
Object.assign(el.dataset, dataset) | |
Object.assign(el, others) | |
for(let child of children) { | |
if (child instanceof Element) { | |
el.appendChild(child) | |
} else { | |
el.appendChild(document.createTextNode(child)) | |
} | |
} | |
return el | |
} | |
// フォーム要素の値を取得したりセットしたりする | |
const form = (target, ...values) => { | |
// get | |
if (values.length === 0) { | |
return query(target)?.value | |
} | |
// set | |
return [query(target)].map(e => { | |
const value = values.reduce((pre, value) => { | |
if (typeof value === "boolean" && e.tagName == "INPUT" && ["checkbox", "radio"].includes(e.type)) { | |
value = { checked: value } | |
} else if (["string", "number", "boolean"].includes(typeof value) || value === null) { | |
value = { value } | |
} | |
if (typeof value === "object") { | |
return Object.assign(pre, value) | |
} | |
}, {}) | |
Object.assign(e, value) | |
return value | |
}).shift() | |
} | |
// ワイルドカードを正規表現化する | |
const wc = str => new RegExp(`^${str.replace(/[^a-z0-9_]/gi, "\\$&").replace(/\\\*/g, ".*")}$`) | |
// urlにマッチしたらcb(url)を実行 | |
const urlmatch = (url, cb = url => console.log(`urlmatch: ${url}`)) => | |
(wc(url)?.test(location.href) || (location.hash == "" && wc(url)?.test(location.href.replace(/#$/, "")))) && cb(url) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ユーザスクリプトのコメントに以下を追記すると使えるようになる
// @require https://gist.githubusercontent.com/kawaz/804ddbcc7ddc5033d09d4e72fdcd3bf5/raw/tools.js?v=1
クエリの
?v=1
はキャッシュクリア用のパラメータ。gist更新後にすぐ使いたい場合はユーザスクリプト側の@require
のクエリを変えればよい。