@vue-composable/web
Advanced tools
Comparing version 1.0.0-alpha.5 to 1.0.0-alpha.7
@@ -79,74 +79,2 @@ 'use strict'; | ||
function useMatchMedia(query) { | ||
const mediaQueryList = runtimeCore.ref(matchMedia(query)); | ||
const matches = runtimeCore.ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
runtimeCore.onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = runtimeCore.ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (core.isNumber(bp)) { | ||
const r = runtimeCore.ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = core.useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
runtimeCore.onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
runtimeCore.onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
function useFetch(options) { | ||
@@ -313,62 +241,419 @@ const json = runtimeCore.ref(null); | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && core.wrap(defaultValue)) || | ||
runtimeCore.ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
function useNetworkInformation() { | ||
const connection = navigator.connection || | ||
navigator.mozConnection || | ||
navigator.webkitConnection; | ||
const supported = runtimeCore.computed(() => !!connection); | ||
const downlink = runtimeCore.ref(0); | ||
const downlinkMax = runtimeCore.ref(0); | ||
const effectiveType = runtimeCore.ref("none"); | ||
const rtt = runtimeCore.ref(0); | ||
const saveData = runtimeCore.ref(false); | ||
const type = runtimeCore.ref("none"); | ||
let handler = core.NO_OP; | ||
let remove = core.NO_OP; | ||
/* istanbul ignore else */ | ||
if (connection) { | ||
handler = () => { | ||
downlink.value = connection.downlink; | ||
downlinkMax.value = connection.downlinkMax; | ||
effectiveType.value = connection.effectiveType; | ||
rtt.value = connection.rtt; | ||
saveData.value = connection.saveData; | ||
type.value = connection.type; | ||
}; | ||
remove = useEvent(connection, "change", handler, { passive: true }); | ||
handler(); | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
{ | ||
console.warn("[navigator.connection] not found, networkInformation not available."); | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
return { | ||
supported, | ||
downlink, | ||
downlinkMax, | ||
effectiveType, | ||
rtt, | ||
saveData, | ||
type, | ||
remove | ||
}; | ||
} | ||
let online = undefined; | ||
function useOnline() { | ||
const supported = "onLine" in navigator; | ||
// not sure how to test this :/ | ||
if (!supported) { | ||
online = runtimeCore.ref(false); | ||
} | ||
if (!online) { | ||
online = runtimeCore.ref(navigator.onLine); | ||
window.addEventListener("offline", () => (online.value = false), { | ||
passive: true | ||
}); | ||
window.addEventListener("online", () => (online.value = true), { | ||
passive: true | ||
}); | ||
} | ||
return { | ||
supported, | ||
online | ||
}; | ||
} | ||
let visibility = undefined; | ||
let hidden = undefined; | ||
function usePageVisibility() { | ||
if (!hidden) { | ||
hidden = runtimeCore.ref(document.hidden); | ||
} | ||
if (!visibility) { | ||
visibility = runtimeCore.ref(document.visibilityState); | ||
document.addEventListener("visibilitychange", () => { | ||
visibility.value = document.visibilityState; | ||
hidden.value = document.hidden; | ||
}, { passive: true } | ||
// true | ||
); | ||
} | ||
return { | ||
visibility, | ||
hidden | ||
}; | ||
} | ||
let language = undefined; | ||
let languages = undefined; | ||
function useLanguage() { | ||
if (!language) { | ||
language = runtimeCore.ref(navigator.language); | ||
} | ||
if (!languages) { | ||
languages = runtimeCore.ref(navigator.languages); | ||
const change = () => { | ||
language.value = navigator.language; | ||
languages.value = navigator.languages; | ||
}; | ||
window.addEventListener('languagechange', change, { passive: true }); | ||
} | ||
return { | ||
language, | ||
languages | ||
}; | ||
} | ||
function useMatchMedia(query) { | ||
const mediaQueryList = runtimeCore.ref(matchMedia(query)); | ||
const matches = runtimeCore.ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
runtimeCore.onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = runtimeCore.ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (core.isNumber(bp)) { | ||
const r = runtimeCore.ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
runtimeCore.watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
core.debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = core.useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
runtimeCore.onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
runtimeCore.onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
/* istanbul ignore next */ | ||
function isQuotaExceededError(e, storage) { | ||
return e instanceof DOMException && ( | ||
// everything except Firefox | ||
e.code === 22 || | ||
// Firefox | ||
e.code === 1014 || | ||
// test name field too, because code might not be present | ||
// everything except Firefox | ||
e.name === 'QuotaExceededError' || | ||
// Firefox | ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && | ||
// acknowledge QuotaExceededError only if there's something already stored | ||
(storage && storage.length !== 0); | ||
} | ||
// based on https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API | ||
function storageAvailable(storage) { | ||
try { | ||
const x = '__storage_test__'; | ||
storage.setItem(x, x); | ||
storage.removeItem(x); | ||
return true; | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
catch (e) { | ||
return isQuotaExceededError(e, storage); | ||
} | ||
} | ||
function safeParse(serializer, value) { | ||
try { | ||
return serializer.parse(value); | ||
} | ||
catch { | ||
return value; | ||
} | ||
} | ||
let storageMap = undefined; | ||
function useWebStorage(type, serializer = JSON, ms = 10) { | ||
const storage = window[type]; | ||
const supported = storageAvailable(storage); | ||
const remove = () => storageMap.delete(type); | ||
if (!storageMap) { | ||
storageMap = new Map(); | ||
window.addEventListener('storage', (e) => { | ||
if (e.newValue === e.oldValue) { | ||
return; | ||
} | ||
weakMap.delete(v); | ||
let webStore = storageMap.get('localStorage'); | ||
if (e.storageArea === window.localStorage) { | ||
webStore = storageMap.get('localStorage'); | ||
} | ||
else { | ||
webStore = storageMap.get('sessionStorage'); | ||
} | ||
if (webStore && Object.keys(webStore.$syncKeys).length > 0) { | ||
if (e.key === null) { | ||
webStore.clear(); | ||
} | ||
else if (webStore.$syncKeys[e.key]) { | ||
if (e.newValue === null) { | ||
webStore.removeItem(e.key); | ||
} | ||
else { | ||
webStore.updateItem(e.key, e.newValue); | ||
} | ||
} | ||
} | ||
}); | ||
keyedMap.clear(); | ||
} | ||
let store = storageMap.get(type); | ||
let quotaError; | ||
if (supported) { | ||
if (!store) { | ||
quotaError = runtimeCore.ref(false); | ||
store = { | ||
$refMap: new Map(), | ||
$watchHandlers: new Map(), | ||
$syncKeys: {}, | ||
$quotaError: quotaError, | ||
key: storage.key, | ||
length: storage.length, | ||
setSync(key, sync) { | ||
if (sync) { | ||
this.$syncKeys[key] = true; | ||
} | ||
else { | ||
delete this.$syncKeys[key]; | ||
} | ||
}, | ||
clear() { | ||
this.$refMap.forEach((_, k) => this.removeItem(k)); | ||
}, | ||
removeItem(k) { | ||
const item = this.$refMap.get(k); | ||
// remove the object value if item deleted | ||
if (item) { | ||
item.value = undefined; | ||
} | ||
// clear the watch | ||
const stop = this.$watchHandlers.get(k); | ||
if (stop) { | ||
stop(); | ||
} | ||
delete this.$syncKeys[k]; | ||
this.$refMap.delete(k); | ||
storage.removeItem(k); | ||
}, | ||
getItem(k) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
return r; | ||
} | ||
const data = storage.getItem(k); | ||
if (!data) { | ||
return null; | ||
} | ||
return this.setItem(k, safeParse(serializer, data)); | ||
}, | ||
setItem(k, v) { | ||
const reference = core.wrap(v); | ||
this.$refMap.set(k, reference); | ||
const save = (key, value) => { | ||
try { | ||
const data = core.isString(value) ? value : serializer.stringify(value); | ||
storage.setItem(key, data); | ||
} | ||
catch (e) { | ||
quotaError.value = isQuotaExceededError(e, storage); | ||
} | ||
}; | ||
save(k, v); | ||
const stop = runtimeCore.watch(() => reference, core.debounce((r) => { | ||
save(k, r.value); | ||
}, ms), { | ||
lazy: true, | ||
deep: true | ||
}); | ||
this.$watchHandlers.set(k, stop); | ||
return reference; | ||
}, | ||
updateItem(k, data) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
r.value = safeParse(serializer, data); | ||
} | ||
} | ||
}; | ||
storageMap.set(type, store); | ||
} | ||
else { | ||
quotaError = store.$quotaError; | ||
} | ||
} | ||
else { | ||
quotaError = runtimeCore.ref(false); | ||
store = {}; | ||
} | ||
return { | ||
supported, | ||
quotaError, | ||
store, | ||
remove | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
} | ||
function useLocalStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('localStorage'); | ||
let remove = core.NO_OP; | ||
let clear = core.NO_OP; | ||
let setSync = core.NO_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
setSync = (s) => store.setSync(key, s); | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
{ | ||
console.warn('[localStorage] is not available'); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove, | ||
setSync, | ||
}; | ||
} | ||
function useSessionStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('sessionStorage'); | ||
let remove = core.NO_OP; | ||
let clear = core.NO_OP; | ||
let setSync = core.FALSE_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
/* istanbul ignore else */ | ||
{ | ||
setSync = () => console.warn('sync is not supported, please `useLocalStorage` instead'); | ||
} | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
{ | ||
console.warn('[sessionStorage] is not available'); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove | ||
remove, | ||
setSync | ||
}; | ||
} | ||
let canUseLocalStorage = undefined; | ||
function useStorage(key, defaultValue) { | ||
if (canUseLocalStorage === undefined) { | ||
canUseLocalStorage = useWebStorage('localStorage').supported; | ||
} | ||
return canUseLocalStorage | ||
? useLocalStorage(key, defaultValue) | ||
: useSessionStorage(key, defaultValue); | ||
} | ||
exports.storageAvailable = storageAvailable; | ||
exports.useBreakpoint = useBreakpoint; | ||
@@ -378,7 +663,14 @@ exports.useEvent = useEvent; | ||
exports.useIntersectionObserver = useIntersectionObserver; | ||
exports.useLanguage = useLanguage; | ||
exports.useLocalStorage = useLocalStorage; | ||
exports.useMatchMedia = useMatchMedia; | ||
exports.useNetworkInformation = useNetworkInformation; | ||
exports.useOnMouseMove = useOnMouseMove; | ||
exports.useOnResize = useOnResize; | ||
exports.useOnScroll = useOnScroll; | ||
exports.useOnline = useOnline; | ||
exports.usePageVisibility = usePageVisibility; | ||
exports.useSessionStorage = useSessionStorage; | ||
exports.useStorage = useStorage; | ||
exports.useWebSocket = useWebSocket; | ||
exports.useWebStorage = useWebStorage; |
@@ -79,74 +79,2 @@ 'use strict'; | ||
function useMatchMedia(query) { | ||
const mediaQueryList = runtimeCore.ref(matchMedia(query)); | ||
const matches = runtimeCore.ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
runtimeCore.onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = runtimeCore.ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (core.isNumber(bp)) { | ||
const r = runtimeCore.ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = core.useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
runtimeCore.onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
runtimeCore.onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
function useFetch(options) { | ||
@@ -301,62 +229,397 @@ const json = runtimeCore.ref(null); | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && core.wrap(defaultValue)) || | ||
runtimeCore.ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
function useNetworkInformation() { | ||
const connection = navigator.connection || | ||
navigator.mozConnection || | ||
navigator.webkitConnection; | ||
const supported = runtimeCore.computed(() => !!connection); | ||
const downlink = runtimeCore.ref(0); | ||
const downlinkMax = runtimeCore.ref(0); | ||
const effectiveType = runtimeCore.ref("none"); | ||
const rtt = runtimeCore.ref(0); | ||
const saveData = runtimeCore.ref(false); | ||
const type = runtimeCore.ref("none"); | ||
let handler = core.NO_OP; | ||
let remove = core.NO_OP; | ||
/* istanbul ignore else */ | ||
if (connection) { | ||
handler = () => { | ||
downlink.value = connection.downlink; | ||
downlinkMax.value = connection.downlinkMax; | ||
effectiveType.value = connection.effectiveType; | ||
rtt.value = connection.rtt; | ||
saveData.value = connection.saveData; | ||
type.value = connection.type; | ||
}; | ||
remove = useEvent(connection, "change", handler, { passive: true }); | ||
handler(); | ||
} | ||
return { | ||
supported, | ||
downlink, | ||
downlinkMax, | ||
effectiveType, | ||
rtt, | ||
saveData, | ||
type, | ||
remove | ||
}; | ||
} | ||
let online = undefined; | ||
function useOnline() { | ||
const supported = "onLine" in navigator; | ||
// not sure how to test this :/ | ||
if (!supported) { | ||
online = runtimeCore.ref(false); | ||
} | ||
if (!online) { | ||
online = runtimeCore.ref(navigator.onLine); | ||
window.addEventListener("offline", () => (online.value = false), { | ||
passive: true | ||
}); | ||
window.addEventListener("online", () => (online.value = true), { | ||
passive: true | ||
}); | ||
} | ||
return { | ||
supported, | ||
online | ||
}; | ||
} | ||
let visibility = undefined; | ||
let hidden = undefined; | ||
function usePageVisibility() { | ||
if (!hidden) { | ||
hidden = runtimeCore.ref(document.hidden); | ||
} | ||
if (!visibility) { | ||
visibility = runtimeCore.ref(document.visibilityState); | ||
document.addEventListener("visibilitychange", () => { | ||
visibility.value = document.visibilityState; | ||
hidden.value = document.hidden; | ||
}, { passive: true } | ||
// true | ||
); | ||
} | ||
return { | ||
visibility, | ||
hidden | ||
}; | ||
} | ||
let language = undefined; | ||
let languages = undefined; | ||
function useLanguage() { | ||
if (!language) { | ||
language = runtimeCore.ref(navigator.language); | ||
} | ||
if (!languages) { | ||
languages = runtimeCore.ref(navigator.languages); | ||
const change = () => { | ||
language.value = navigator.language; | ||
languages.value = navigator.languages; | ||
}; | ||
window.addEventListener('languagechange', change, { passive: true }); | ||
} | ||
return { | ||
language, | ||
languages | ||
}; | ||
} | ||
function useMatchMedia(query) { | ||
const mediaQueryList = runtimeCore.ref(matchMedia(query)); | ||
const matches = runtimeCore.ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
runtimeCore.onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = runtimeCore.ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (core.isNumber(bp)) { | ||
const r = runtimeCore.ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
runtimeCore.watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
core.debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = core.useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
runtimeCore.onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
runtimeCore.onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
/* istanbul ignore next */ | ||
function isQuotaExceededError(e, storage) { | ||
return e instanceof DOMException && ( | ||
// everything except Firefox | ||
e.code === 22 || | ||
// Firefox | ||
e.code === 1014 || | ||
// test name field too, because code might not be present | ||
// everything except Firefox | ||
e.name === 'QuotaExceededError' || | ||
// Firefox | ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && | ||
// acknowledge QuotaExceededError only if there's something already stored | ||
(storage && storage.length !== 0); | ||
} | ||
// based on https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API | ||
function storageAvailable(storage) { | ||
try { | ||
const x = '__storage_test__'; | ||
storage.setItem(x, x); | ||
storage.removeItem(x); | ||
return true; | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
catch (e) { | ||
return isQuotaExceededError(e, storage); | ||
} | ||
} | ||
function safeParse(serializer, value) { | ||
try { | ||
return serializer.parse(value); | ||
} | ||
catch { | ||
return value; | ||
} | ||
} | ||
let storageMap = undefined; | ||
function useWebStorage(type, serializer = JSON, ms = 10) { | ||
const storage = window[type]; | ||
const supported = storageAvailable(storage); | ||
const remove = () => storageMap.delete(type); | ||
if (!storageMap) { | ||
storageMap = new Map(); | ||
window.addEventListener('storage', (e) => { | ||
if (e.newValue === e.oldValue) { | ||
return; | ||
} | ||
weakMap.delete(v); | ||
let webStore = storageMap.get('localStorage'); | ||
if (e.storageArea === window.localStorage) { | ||
webStore = storageMap.get('localStorage'); | ||
} | ||
else { | ||
webStore = storageMap.get('sessionStorage'); | ||
} | ||
if (webStore && Object.keys(webStore.$syncKeys).length > 0) { | ||
if (e.key === null) { | ||
webStore.clear(); | ||
} | ||
else if (webStore.$syncKeys[e.key]) { | ||
if (e.newValue === null) { | ||
webStore.removeItem(e.key); | ||
} | ||
else { | ||
webStore.updateItem(e.key, e.newValue); | ||
} | ||
} | ||
} | ||
}); | ||
keyedMap.clear(); | ||
} | ||
let store = storageMap.get(type); | ||
let quotaError; | ||
if (supported) { | ||
if (!store) { | ||
quotaError = runtimeCore.ref(false); | ||
store = { | ||
$refMap: new Map(), | ||
$watchHandlers: new Map(), | ||
$syncKeys: {}, | ||
$quotaError: quotaError, | ||
key: storage.key, | ||
length: storage.length, | ||
setSync(key, sync) { | ||
if (sync) { | ||
this.$syncKeys[key] = true; | ||
} | ||
else { | ||
delete this.$syncKeys[key]; | ||
} | ||
}, | ||
clear() { | ||
this.$refMap.forEach((_, k) => this.removeItem(k)); | ||
}, | ||
removeItem(k) { | ||
const item = this.$refMap.get(k); | ||
// remove the object value if item deleted | ||
if (item) { | ||
item.value = undefined; | ||
} | ||
// clear the watch | ||
const stop = this.$watchHandlers.get(k); | ||
if (stop) { | ||
stop(); | ||
} | ||
delete this.$syncKeys[k]; | ||
this.$refMap.delete(k); | ||
storage.removeItem(k); | ||
}, | ||
getItem(k) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
return r; | ||
} | ||
const data = storage.getItem(k); | ||
if (!data) { | ||
return null; | ||
} | ||
return this.setItem(k, safeParse(serializer, data)); | ||
}, | ||
setItem(k, v) { | ||
const reference = core.wrap(v); | ||
this.$refMap.set(k, reference); | ||
const save = (key, value) => { | ||
try { | ||
const data = core.isString(value) ? value : serializer.stringify(value); | ||
storage.setItem(key, data); | ||
} | ||
catch (e) { | ||
quotaError.value = isQuotaExceededError(e, storage); | ||
} | ||
}; | ||
save(k, v); | ||
const stop = runtimeCore.watch(() => reference, core.debounce((r) => { | ||
save(k, r.value); | ||
}, ms), { | ||
lazy: true, | ||
deep: true | ||
}); | ||
this.$watchHandlers.set(k, stop); | ||
return reference; | ||
}, | ||
updateItem(k, data) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
r.value = safeParse(serializer, data); | ||
} | ||
} | ||
}; | ||
storageMap.set(type, store); | ||
} | ||
else { | ||
quotaError = store.$quotaError; | ||
} | ||
} | ||
else { | ||
quotaError = runtimeCore.ref(false); | ||
store = {}; | ||
} | ||
return { | ||
supported, | ||
quotaError, | ||
store, | ||
remove | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
} | ||
function useLocalStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('localStorage'); | ||
let remove = core.NO_OP; | ||
let clear = core.NO_OP; | ||
let setSync = core.NO_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
setSync = (s) => store.setSync(key, s); | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove, | ||
setSync, | ||
}; | ||
} | ||
function useSessionStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('sessionStorage'); | ||
let remove = core.NO_OP; | ||
let clear = core.NO_OP; | ||
let setSync = core.FALSE_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove | ||
remove, | ||
setSync | ||
}; | ||
} | ||
let canUseLocalStorage = undefined; | ||
function useStorage(key, defaultValue) { | ||
if (canUseLocalStorage === undefined) { | ||
canUseLocalStorage = useWebStorage('localStorage').supported; | ||
} | ||
return canUseLocalStorage | ||
? useLocalStorage(key, defaultValue) | ||
: useSessionStorage(key, defaultValue); | ||
} | ||
exports.storageAvailable = storageAvailable; | ||
exports.useBreakpoint = useBreakpoint; | ||
@@ -366,7 +629,14 @@ exports.useEvent = useEvent; | ||
exports.useIntersectionObserver = useIntersectionObserver; | ||
exports.useLanguage = useLanguage; | ||
exports.useLocalStorage = useLocalStorage; | ||
exports.useMatchMedia = useMatchMedia; | ||
exports.useNetworkInformation = useNetworkInformation; | ||
exports.useOnMouseMove = useOnMouseMove; | ||
exports.useOnResize = useOnResize; | ||
exports.useOnScroll = useOnScroll; | ||
exports.useOnline = useOnline; | ||
exports.usePageVisibility = usePageVisibility; | ||
exports.useSessionStorage = useSessionStorage; | ||
exports.useStorage = useStorage; | ||
exports.useWebSocket = useWebSocket; | ||
exports.useWebStorage = useWebStorage; |
@@ -20,2 +20,3 @@ import { Ref } from '@vue/runtime-core'; | ||
export declare interface LocalStorageReturn<T> { | ||
supported: boolean; | ||
storage: Ref<T>; | ||
@@ -30,5 +31,9 @@ /** | ||
clear: () => void; | ||
/** | ||
* @description Enable cross tab syncing | ||
*/ | ||
setSync: (sync: boolean) => void; | ||
} | ||
export declare type LocalStorageTyped<T> = string; | ||
export declare type LocalStorageTyped<T extends object> = string; | ||
@@ -41,2 +46,54 @@ export declare interface MouseMoveResult { | ||
export declare interface NetworkInformation { | ||
readonly downlink: number; | ||
readonly downlinkMax: number; | ||
readonly effectiveType: NetworkInformationEffectiveType; | ||
readonly rtt: number; | ||
readonly saveData: Boolean; | ||
readonly type: NetworkInformationType; | ||
onchange: (this: NetworkInformation, ev: Event) => void; | ||
addEventListener<K extends keyof NetworkInformationEventMap>(type: K, listener: (this: NetworkInformation, ev: NetworkInformationEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; | ||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; | ||
removeEventListener<K extends keyof NetworkInformationEventMap>(type: K, listener: (this: NetworkInformation, ev: NetworkInformationEventMap[K]) => any, options?: boolean | EventListenerOptions): void; | ||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; | ||
} | ||
declare type NetworkInformationEffectiveType = "slow-2g" | "2g" | "3g" | "4g" | "none"; | ||
declare interface NetworkInformationEventMap { | ||
change: Event; | ||
} | ||
declare interface NetworkInformationReturn { | ||
readonly supported: Ref<boolean>; | ||
/** | ||
* @description Returns the effective bandwidth estimate in megabits per second, rounded to the nearest multiple of 25 kilobits per seconds | ||
*/ | ||
readonly downlink: Ref<number>; | ||
/** | ||
* @description Returns the maximum downlink speed, in megabits per second (Mbps), for the underlying connection technology. | ||
*/ | ||
readonly downlinkMax: Ref<number>; | ||
/** | ||
* @description Returns the effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. This value is determined using a combination of recently observed round-trip time and downlink values. | ||
*/ | ||
readonly effectiveType: Ref<NetworkInformationEffectiveType>; | ||
/** | ||
* @description Returns the estimated effective round-trip time of the current connection, rounded to the nearest multiple of 25 milliseconds. | ||
*/ | ||
readonly rtt: Ref<number>; | ||
/** | ||
* @description Returns true if the user has set a reduced data usage option on the user agent. | ||
*/ | ||
readonly saveData: Ref<Boolean>; | ||
/** | ||
* @description Returns the type of connection a device is using to communicate with the network. It will be one of the following values: | ||
* @enum { NetworkInformationType} | ||
*/ | ||
readonly type: Ref<NetworkInformationType>; | ||
remove: RemoveEventFunction; | ||
} | ||
declare type NetworkInformationType = "bluetooth" | "cellular" | "ethernet" | "mixed" | "none" | "other" | "unknown" | "wifi" | "wimax"; | ||
export declare type RemoveEventFunction = () => void; | ||
@@ -56,2 +113,9 @@ | ||
export declare function storageAvailable(storage: Storage): boolean; | ||
export declare interface StorageSerializer<T = any> { | ||
stringify(item: T): string; | ||
parse(data: string): T; | ||
} | ||
export declare function useBreakpoint<T>(breakpoints: Record<keyof T, number | string>): Record<keyof T, Ref<boolean>> & { | ||
@@ -62,2 +126,12 @@ remove: RemoveEventFunction; | ||
export declare function useEvent<T extends { | ||
addEventListener: (name: string, listener: EventListenerOrEventListenerObject) => any; | ||
removeEventListener: Function; | ||
}, M, K extends keyof M>(el: RefTyped<T>, name: K, listener: (this: T, ev: M[K]) => any): RemoveEventFunction; | ||
export declare function useEvent<T extends { | ||
addEventListener: (name: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => any; | ||
removeEventListener: Function; | ||
}, M, K extends keyof M>(el: RefTyped<T>, name: K, listener: (this: T, ev: M[K]) => any, options?: boolean | AddEventListenerOptions): RemoveEventFunction; | ||
export declare function useEvent<K extends keyof WindowEventMap>(el: RefTyped<Window>, name: K, listener: (this: Document, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): RemoveEventFunction; | ||
@@ -96,4 +170,11 @@ | ||
export declare function useLocalStorage<T = any>(key: LocalStorageTyped<T> | string, defaultValue?: RefTyped<T>): LocalStorageReturn<T>; | ||
export declare function useLanguage(): { | ||
language: Ref<String>; | ||
languages: Ref<readonly String[]>; | ||
}; | ||
export declare function useLocalStorage(key: string, defaultValue?: RefTyped<string>): LocalStorageReturn<string>; | ||
export declare function useLocalStorage<T extends object = any>(key: LocalStorageTyped<T> | string, defaultValue?: RefTyped<T>): LocalStorageReturn<T>; | ||
export declare function useMatchMedia(query: string): { | ||
@@ -105,2 +186,9 @@ mediaQueryList: import("@vue/reactivity").Ref<MediaQueryList>; | ||
export declare function useNetworkInformation(): NetworkInformationReturn; | ||
export declare function useOnline(): { | ||
supported: boolean; | ||
online: Ref<boolean>; | ||
}; | ||
export declare function useOnMouseMove(el: RefTyped<Window>, wait: number): MouseMoveResult; | ||
@@ -130,2 +218,15 @@ | ||
export declare function usePageVisibility(): { | ||
visibility: Ref<VisibilityState>; | ||
hidden: Ref<boolean>; | ||
}; | ||
export declare function useSessionStorage(key: string, defaultValue?: RefTyped<string>): LocalStorageReturn<string>; | ||
export declare function useSessionStorage<T extends object = object>(key: LocalStorageTyped<T> | string, defaultValue?: RefTyped<T>): LocalStorageReturn<T>; | ||
export declare function useStorage(key: string, defaultValue?: RefTyped<string>): LocalStorageReturn<string>; | ||
export declare function useStorage<T extends object = any>(key: LocalStorageTyped<T> | string, defaultValue?: RefTyped<T>): LocalStorageReturn<T>; | ||
export declare function useWebSocket(url: string, protocols?: string | string[]): { | ||
@@ -143,2 +244,46 @@ ws: WebSocket; | ||
export declare function useWebStorage(type: WebStorageType, serializer?: StorageSerializer, ms?: number): { | ||
supported: boolean; | ||
quotaError: Ref<boolean>; | ||
store: WebStorage; | ||
remove: () => boolean; | ||
}; | ||
export declare interface WebStorage { | ||
$refMap: Map<string, Ref<any>>; | ||
$watchHandlers: Map<string, Function>; | ||
$syncKeys: Record<string, boolean>; | ||
$quotaError: Ref<boolean>; | ||
updateItem(key: string, value: string): void; | ||
setSync(key: string, sync: boolean): void; | ||
/** | ||
* Returns the number of key/value pairs currently present in the list associated with the object. | ||
*/ | ||
readonly length: number; | ||
/** | ||
* Empties the list associated with the object of all key/value pairs, if there are any. | ||
*/ | ||
clear(): void; | ||
/** | ||
* Returns the current value associated with the given key, or null if the given key does not exist in the list associated with the object. | ||
*/ | ||
getItem<T = any>(key: string): Ref<T> | null; | ||
/** | ||
* Returns the name of the nth key in the list, or null if n is greater than or equal to the number of key/value pairs in the object. | ||
*/ | ||
key(index: number): string | null; | ||
/** | ||
* Removes the key/value pair with the given key from the list associated with the object, if a key/value pair with the given key exists. | ||
*/ | ||
removeItem(key: string): void; | ||
/** | ||
* Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. | ||
* | ||
* Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) | ||
*/ | ||
setItem<T>(key: string, value: T): Ref<T>; | ||
} | ||
declare type WebStorageType = 'localStorage' | 'sessionStorage'; | ||
export { } |
import { onMounted, onUnmounted, ref, computed, watch } from '@vue/runtime-core'; | ||
import { wrap, useDebounce, isNumber, usePromise, isElement, unwrap, debounce } from '@vue-composable/core'; | ||
import { wrap, useDebounce, usePromise, isElement, unwrap, NO_OP, isNumber, debounce, isString, FALSE_OP } from '@vue-composable/core'; | ||
@@ -75,74 +75,2 @@ function useEvent(el, name, listener, options) { | ||
function useMatchMedia(query) { | ||
const mediaQueryList = ref(matchMedia(query)); | ||
const matches = ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (isNumber(bp)) { | ||
const r = ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
function useFetch(options) { | ||
@@ -309,62 +237,418 @@ const json = ref(null); | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && wrap(defaultValue)) || | ||
ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
function useNetworkInformation() { | ||
const connection = navigator.connection || | ||
navigator.mozConnection || | ||
navigator.webkitConnection; | ||
const supported = computed(() => !!connection); | ||
const downlink = ref(0); | ||
const downlinkMax = ref(0); | ||
const effectiveType = ref("none"); | ||
const rtt = ref(0); | ||
const saveData = ref(false); | ||
const type = ref("none"); | ||
let handler = NO_OP; | ||
let remove = NO_OP; | ||
/* istanbul ignore else */ | ||
if (connection) { | ||
handler = () => { | ||
downlink.value = connection.downlink; | ||
downlinkMax.value = connection.downlinkMax; | ||
effectiveType.value = connection.effectiveType; | ||
rtt.value = connection.rtt; | ||
saveData.value = connection.saveData; | ||
type.value = connection.type; | ||
}; | ||
remove = useEvent(connection, "change", handler, { passive: true }); | ||
handler(); | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
if ((true !== 'production')) { | ||
console.warn("[navigator.connection] not found, networkInformation not available."); | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
return { | ||
supported, | ||
downlink, | ||
downlinkMax, | ||
effectiveType, | ||
rtt, | ||
saveData, | ||
type, | ||
remove | ||
}; | ||
} | ||
let online = undefined; | ||
function useOnline() { | ||
const supported = "onLine" in navigator; | ||
// not sure how to test this :/ | ||
if (!supported) { | ||
online = ref(false); | ||
} | ||
if (!online) { | ||
online = ref(navigator.onLine); | ||
window.addEventListener("offline", () => (online.value = false), { | ||
passive: true | ||
}); | ||
window.addEventListener("online", () => (online.value = true), { | ||
passive: true | ||
}); | ||
} | ||
return { | ||
supported, | ||
online | ||
}; | ||
} | ||
let visibility = undefined; | ||
let hidden = undefined; | ||
function usePageVisibility() { | ||
if (!hidden) { | ||
hidden = ref(document.hidden); | ||
} | ||
if (!visibility) { | ||
visibility = ref(document.visibilityState); | ||
document.addEventListener("visibilitychange", () => { | ||
visibility.value = document.visibilityState; | ||
hidden.value = document.hidden; | ||
}, { passive: true } | ||
// true | ||
); | ||
} | ||
return { | ||
visibility, | ||
hidden | ||
}; | ||
} | ||
let language = undefined; | ||
let languages = undefined; | ||
function useLanguage() { | ||
if (!language) { | ||
language = ref(navigator.language); | ||
} | ||
if (!languages) { | ||
languages = ref(navigator.languages); | ||
const change = () => { | ||
language.value = navigator.language; | ||
languages.value = navigator.languages; | ||
}; | ||
window.addEventListener('languagechange', change, { passive: true }); | ||
} | ||
return { | ||
language, | ||
languages | ||
}; | ||
} | ||
function useMatchMedia(query) { | ||
const mediaQueryList = ref(matchMedia(query)); | ||
const matches = ref(mediaQueryList.value.matches); | ||
const process = (e) => { | ||
matches.value = e.matches; | ||
}; | ||
mediaQueryList.value.addEventListener("change", process, { passive: true }); | ||
const remove = () => mediaQueryList.value.removeEventListener("change", process); | ||
onUnmounted(remove); | ||
return { | ||
mediaQueryList, | ||
remove, | ||
matches | ||
}; | ||
} | ||
function useBreakpoint(breakpoints) { | ||
const result = {}; | ||
const map = new Map(); | ||
const current = ref(); | ||
let sorted = []; | ||
const removeMedia = []; | ||
for (const key in breakpoints) { | ||
const bp = breakpoints[key]; | ||
if (isNumber(bp)) { | ||
const r = ref(false); | ||
result[key] = r; | ||
map.set(bp, { | ||
name: key, | ||
valid: r | ||
}); | ||
sorted.push(bp); | ||
} | ||
else { | ||
const { matches, remove } = useMatchMedia(bp); | ||
result[key] = matches; | ||
removeMedia.push(remove); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
sorted = sorted.sort((a, b) => b - a); | ||
const resize = () => { | ||
const width = window.innerWidth; | ||
let c = undefined; | ||
for (let i = 0; i < sorted.length; i++) { | ||
const bp = sorted[i]; | ||
const r = map.get(bp); | ||
r.valid.value = width >= bp; | ||
if (width >= bp && c === undefined) { | ||
c = r.name; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
} | ||
current.value = c; | ||
}; | ||
const processResize = useDebounce(resize, 10); | ||
const remove = () => window.removeEventListener("resize", processResize); | ||
onMounted(() => { | ||
resize(); | ||
window.addEventListener("resize", processResize, { | ||
passive: true | ||
}); | ||
}); | ||
onUnmounted(() => { | ||
remove(); | ||
removeMedia.forEach(x => x()); | ||
}); | ||
return { | ||
...result, | ||
remove, | ||
current | ||
}; | ||
} | ||
/* istanbul ignore next */ | ||
function isQuotaExceededError(e, storage) { | ||
return e instanceof DOMException && ( | ||
// everything except Firefox | ||
e.code === 22 || | ||
// Firefox | ||
e.code === 1014 || | ||
// test name field too, because code might not be present | ||
// everything except Firefox | ||
e.name === 'QuotaExceededError' || | ||
// Firefox | ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && | ||
// acknowledge QuotaExceededError only if there's something already stored | ||
(storage && storage.length !== 0); | ||
} | ||
// based on https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API | ||
function storageAvailable(storage) { | ||
try { | ||
const x = '__storage_test__'; | ||
storage.setItem(x, x); | ||
storage.removeItem(x); | ||
return true; | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
catch (e) { | ||
return isQuotaExceededError(e, storage); | ||
} | ||
} | ||
function safeParse(serializer, value) { | ||
try { | ||
return serializer.parse(value); | ||
} | ||
catch { | ||
return value; | ||
} | ||
} | ||
let storageMap = undefined; | ||
function useWebStorage(type, serializer = JSON, ms = 10) { | ||
const storage = window[type]; | ||
const supported = storageAvailable(storage); | ||
const remove = () => storageMap.delete(type); | ||
if (!storageMap) { | ||
storageMap = new Map(); | ||
window.addEventListener('storage', (e) => { | ||
if (e.newValue === e.oldValue) { | ||
return; | ||
} | ||
weakMap.delete(v); | ||
let webStore = storageMap.get('localStorage'); | ||
if (e.storageArea === window.localStorage) { | ||
webStore = storageMap.get('localStorage'); | ||
} | ||
else { | ||
webStore = storageMap.get('sessionStorage'); | ||
} | ||
if (webStore && Object.keys(webStore.$syncKeys).length > 0) { | ||
if (e.key === null) { | ||
webStore.clear(); | ||
} | ||
else if (webStore.$syncKeys[e.key]) { | ||
if (e.newValue === null) { | ||
webStore.removeItem(e.key); | ||
} | ||
else { | ||
webStore.updateItem(e.key, e.newValue); | ||
} | ||
} | ||
} | ||
}); | ||
keyedMap.clear(); | ||
} | ||
let store = storageMap.get(type); | ||
let quotaError; | ||
if (supported) { | ||
if (!store) { | ||
quotaError = ref(false); | ||
store = { | ||
$refMap: new Map(), | ||
$watchHandlers: new Map(), | ||
$syncKeys: {}, | ||
$quotaError: quotaError, | ||
key: storage.key, | ||
length: storage.length, | ||
setSync(key, sync) { | ||
if (sync) { | ||
this.$syncKeys[key] = true; | ||
} | ||
else { | ||
delete this.$syncKeys[key]; | ||
} | ||
}, | ||
clear() { | ||
this.$refMap.forEach((_, k) => this.removeItem(k)); | ||
}, | ||
removeItem(k) { | ||
const item = this.$refMap.get(k); | ||
// remove the object value if item deleted | ||
if (item) { | ||
item.value = undefined; | ||
} | ||
// clear the watch | ||
const stop = this.$watchHandlers.get(k); | ||
if (stop) { | ||
stop(); | ||
} | ||
delete this.$syncKeys[k]; | ||
this.$refMap.delete(k); | ||
storage.removeItem(k); | ||
}, | ||
getItem(k) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
return r; | ||
} | ||
const data = storage.getItem(k); | ||
if (!data) { | ||
return null; | ||
} | ||
return this.setItem(k, safeParse(serializer, data)); | ||
}, | ||
setItem(k, v) { | ||
const reference = wrap(v); | ||
this.$refMap.set(k, reference); | ||
const save = (key, value) => { | ||
try { | ||
const data = isString(value) ? value : serializer.stringify(value); | ||
storage.setItem(key, data); | ||
} | ||
catch (e) { | ||
quotaError.value = isQuotaExceededError(e, storage); | ||
} | ||
}; | ||
save(k, v); | ||
const stop = watch(() => reference, debounce((r) => { | ||
save(k, r.value); | ||
}, ms), { | ||
lazy: true, | ||
deep: true | ||
}); | ||
this.$watchHandlers.set(k, stop); | ||
return reference; | ||
}, | ||
updateItem(k, data) { | ||
let r = this.$refMap.get(k); | ||
if (r) { | ||
r.value = safeParse(serializer, data); | ||
} | ||
} | ||
}; | ||
storageMap.set(type, store); | ||
} | ||
else { | ||
quotaError = store.$quotaError; | ||
} | ||
} | ||
else { | ||
quotaError = ref(false); | ||
store = {}; | ||
} | ||
return { | ||
supported, | ||
quotaError, | ||
store, | ||
remove | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
} | ||
function useLocalStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('localStorage'); | ||
let remove = NO_OP; | ||
let clear = NO_OP; | ||
let setSync = NO_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
setSync = (s) => store.setSync(key, s); | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
if ((true !== 'production')) { | ||
console.warn('[localStorage] is not available'); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove, | ||
setSync, | ||
}; | ||
} | ||
function useSessionStorage(key, defaultValue) { | ||
const { supported, store } = useWebStorage('sessionStorage'); | ||
let remove = NO_OP; | ||
let clear = NO_OP; | ||
let setSync = FALSE_OP; | ||
let storage = undefined; | ||
if (supported && store) { | ||
/* istanbul ignore else */ | ||
if ((true !== 'production')) { | ||
setSync = () => console.warn('sync is not supported, please `useLocalStorage` instead'); | ||
} | ||
remove = () => store.removeItem(key); | ||
clear = () => store.clear(); | ||
storage = store.getItem(key); | ||
if (!storage) { | ||
storage = store.setItem(key, defaultValue); | ||
} | ||
} | ||
else { | ||
/* istanbul ignore else */ | ||
if ((true !== 'production')) { | ||
console.warn('[sessionStorage] is not available'); | ||
} | ||
} | ||
return { | ||
supported, | ||
storage, | ||
clear, | ||
remove | ||
remove, | ||
setSync | ||
}; | ||
} | ||
export { useBreakpoint, useEvent, useFetch, useIntersectionObserver, useLocalStorage, useMatchMedia, useOnMouseMove, useOnResize, useOnScroll, useWebSocket }; | ||
let canUseLocalStorage = undefined; | ||
function useStorage(key, defaultValue) { | ||
if (canUseLocalStorage === undefined) { | ||
canUseLocalStorage = useWebStorage('localStorage').supported; | ||
} | ||
return canUseLocalStorage | ||
? useLocalStorage(key, defaultValue) | ||
: useSessionStorage(key, defaultValue); | ||
} | ||
export { storageAvailable, useBreakpoint, useEvent, useFetch, useIntersectionObserver, useLanguage, useLocalStorage, useMatchMedia, useNetworkInformation, useOnMouseMove, useOnResize, useOnScroll, useOnline, usePageVisibility, useSessionStorage, useStorage, useWebSocket, useWebStorage }; |
{ | ||
"name": "@vue-composable/web", | ||
"version": "1.0.0-alpha.5", | ||
"version": "1.0.0-alpha.7", | ||
"description": "@vue-composable/web", | ||
@@ -38,5 +38,5 @@ "main": "index.js", | ||
"dependencies": { | ||
"@vue-composable/core": "1.0.0-alpha.5", | ||
"@vue/runtime-core": "^3.0.0-alpha.1" | ||
"@vue-composable/core": "1.0.0-alpha.7", | ||
"@vue/runtime-core": "^3.0.0-alpha.2" | ||
} | ||
} |
@@ -36,6 +36,12 @@ # @vue-composable/web | ||
- [localStorage](https://pikax.me/vue-composable/composable/misc/localStorage) - Reactive access to a `localStorage` | ||
- [matchMedia](https://pikax.me/vue-composable/composable/misc/matchMedia) - Reactive `matchMedia` | ||
- [breakpoint](https://pikax.me/vue-composable/composable/misc/breakpoint) - reactive `breakpoints` based on `window.innerWidth` | ||
### Storage | ||
- [WebStorage](https://pikax.me/vue-composable/composable/storage/webStorage) - Reactive access to `Storage API`, `useLocalStorage` and `useSessionStorage` use this | ||
- [storage](https://pikax.me/vue-composable/composable/storage/storage) - uses `localStorage` or on safari private it uses `sessionStorage` | ||
- [localStorage](https://pikax.me/vue-composable/composable/storage/localStorage) - Reactive access to a `localStorage` | ||
- [sessionStorage](https://pikax.me/vue-composable/composable/storage/sessionStorage) - Reactive access to a `sessionStorage` | ||
### Web | ||
@@ -46,2 +52,6 @@ | ||
- [IntersectionObserver](https://pikax.me/vue-composable/composable/web/intersectionObserver) - reactive `IntersectionObserver` | ||
- [NetworkInformation](https://pikax.me/vue-composable/composable/web/networkInformation) - reactive `NetworkInformation` wrapper | ||
- [Online](<[composable/web](https://pikax.me/vue-composable/composable/web)/online>) - reactive `navigator.onLine` wrapper | ||
- [PageVisibility](https://pikax.me/vue-composable/composable/web/pageVisibility) - reactive `Page Visibility API` | ||
- [Language](https://pikax.me/vue-composable/composable/web/language) - reactive `NavigatorLanguage` | ||
@@ -48,0 +58,0 @@ ## Contributing |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
79718
7
2141
67
1
+ Added@vue-composable/core@1.0.0-alpha.7(transitive)
- Removed@vue-composable/core@1.0.0-alpha.5(transitive)