@sunnysideup/npm-page-favourites-bookmarker
Version:
NPM package - to bookmark / favourite any page on a website
237 lines (213 loc) • 6.78 kB
JavaScript
import { Store } from './store.js'
import { makeAlphaNumCode, sanitizeHtml, stripTags, toRelativeUrl } from './utils.js'
export class State {
/**
* @param {
* {
* storage:'local'|'session',
* storageKey:string
* }
* } opts
*/
constructor (opts = {}) {
this.opts = opts
this.store = new Store(opts) // use caller's choice
this.bookmarkStorageKey = opts.storageKey + '_bookmarks'
this.codeStorageKey = opts.storageKey + '_code'
this.shareLinkStorageKey = opts.storageKey + '_share_link'
// initial load
this.bookmarks = this.store.getJSON(this.bookmarkStorageKey) || []
this.code = this.store.get(this.codeStorageKey) || makeAlphaNumCode(12)
this.shareLink = this.store.get(this.shareLinkStorageKey) || ''
this.listeners = new Set()
// cross-tab / external updates
this.store.onChange(
key => {
let changed = false
if (key === this.bookmarkStorageKey) {
const next = this.store.getJSON(this.bookmarkStorageKey) || []
if (next.length !== this.bookmarks.length ||
next.some((b, i) => b.url !== this.bookmarks[i]?.url || b.ts !== this.bookmarks[i]?.ts)) {
this.bookmarks = next
changed = true
}
} else if (key === this.codeStorageKey) {
const next = this.store.get(this.codeStorageKey) || ''
if (next !== this.code) { this.code = next; changed = true }
}
// else if (key === this.shareLinkStorageKey) {
// const next = this.store.get(this.shareLinkStorageKey) || ''
// if (next !== this.shareLink) { this.shareLink = next; changed = true }
// }
if (changed) this.#emit()
}
)
}
#loggedInUserHasBeenSyncedFromServer = false
get loggedInUserHasBeenSyncedFromServer () {
return this.#loggedInUserHasBeenSyncedFromServer
}
set loggedInUserHasBeenSyncedFromServer (v) {
this.#loggedInUserHasBeenSyncedFromServer = Boolean(v)
}
onChange (fn) {
this.listeners.add(fn)
return () => this.listeners.delete(fn)
}
list () {
return this.bookmarks.map(b => ({ ...b }))
}
has (url) {
url = toRelativeUrl(url)
if (!url) return false
return this.bookmarks.some(b => b.url === url)
}
add (url, title, imagelink, description) {
const { newUrl, newTitle, newImagelink, newDescription } =
this.#testVarsForBookmark({ url, title, imagelink, description })
if (!newUrl || !newTitle) return false
if (this.has(newUrl)) return false
this.bookmarks.push({
url: newUrl,
title: newTitle,
imagelink: newImagelink,
description: newDescription,
ts: Date.now()
})
this.persist()
return true
}
remove (url, index = null) {
const before = this.bookmarks.length
this.bookmarks = this.bookmarks.filter(b => b.url !== url)
let after = this.bookmarks.length
if(before === after) {
if( index !== null && index >= 0 && index < this.bookmarks.length) {
this.bookmarks.splice(index, 1)
}
}
after = this.bookmarks.length
if (after !== before) {
this.persist()
return true
}
return false
}
reorder (from, to) {
const len = this.bookmarks.length
if (!Number.isInteger(from) || !Number.isInteger(to)) return
if (from < 0 || from >= len || to < 0 || to >= len || from === to) return
const arr = [...this.bookmarks]
const [m] = arr.splice(from, 1)
if (!m) return
arr.splice(to, 0, m)
this.bookmarks = arr
this.persist()
}
/**
* Clear stored data.
* @param {{ keepCode?: boolean, keepShareLink?: boolean, keepBookmarks?: boolean }} [opts]
*/
clear (opts = {}) {
if (!opts.keepCode) {
this.code = ''
}
if (!opts.keepShareLink) {
this.shareLink = ''
}
if (!opts.keepBookmarks) {
this.bookmarks = []
}
this.persist() //
return true
}
setCodeAndShareLink (data) {
this.#setCode(data.code || '')
this.#setShareLink(data.shareLink || '')
}
getCode () {
return this.code || this.store.get(this.codeStorageKey)
}
getShareLink () {
return this.shareLink || this.store.get(this.shareLinkStorageKey)
}
persist () {
try {
this.store.setJSON(this.bookmarkStorageKey, this.bookmarks)
this.store.set(this.codeStorageKey, this.code)
this.store.set(this.shareLinkStorageKey, this.shareLink)
this.#emit()
} catch (e) {
console.error('Persist failed', e)
}
}
mergeFromShareIfAvailable () {
const sharedBookmarks = this.store.getTemporarySharedData()
if (sharedBookmarks) {
this.mergeFromServer(sharedBookmarks, true)
this.store.removeTemporarySharedData()
return true
}
return false
}
mergeFromServer (serverList = {}, fullReplace = false) {
if (typeof serverList.code === 'string' && serverList.code.trim()) {
this.code = serverList.code.trim()
}
const map = fullReplace ? new Map() : new Map(this.bookmarks.map(b => [b.url, b]))
if (Array.isArray(serverList.bookmarks)) {
for (const { url, title, imagelink, description, ts } of serverList.bookmarks) {
const { newUrl, newTitle, newImagelink, newDescription } =
this.#testVarsForBookmark({ url, title, imagelink, description })
if (!newUrl || !newTitle) continue
map.set(newUrl, {
url: newUrl,
title: newTitle,
imagelink: newImagelink,
description: newDescription,
ts: Number.isFinite(ts) ? ts : Date.now()
})
}
}
this.bookmarks = [...map.values()]
this.persist()
}
#setCode (code) {
if (typeof code !== 'string' || !code.trim()) {
if (!this.code) {
this.code = makeAlphaNumCode(12)
}
} else {
this.code = code.trim()
}
this.store.set(this.codeStorageKey, this.code)
}
#setShareLink (link) {
if (typeof link !== 'string' || !link.trim()) {
if (!this.shareLink) {
this.shareLink = ''
}
} else {
this.shareLink = link.trim()
}
this.store.set(this.shareLinkStorageKey, this.shareLink)
}
#emit () {
this.listeners.forEach(fn => fn(this))
}
#testVarsForBookmark ({ url = '', title = '', imagelink = '', description = '' } = {}) {
const newUrl = toRelativeUrl(url)
if (!newUrl) {
console.warn('Invalid URL:', url)
return {}
}
const newTitle = stripTags(title)
if (!newTitle) {
console.warn('Invalid Title:', title)
return {}
}
const newImagelink = toRelativeUrl(imagelink) // may be ''
const newDescription = sanitizeHtml(description)
return { newUrl, newTitle, newImagelink, newDescription }
}
}