Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move history and navigate plugins into livewire/livewire until they are stable #3720

Merged
merged 1 commit into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<script src="./packages/focus/dist/cdn.js"></script>
<script src="./packages/mask/dist/cdn.js"></script>
<script src="./packages/ui/dist/cdn.js" defer></script> -->
<script src="./packages/history/dist/cdn.js"></script>
<script src="./packages/alpinejs/dist/cdn.js" defer></script>
<!-- <script src="//cdn.tailwindcss.com"></script> -->
<!-- <script src="//cdn.tailwindcss.com"></script> -->
Expand Down
238 changes: 1 addition & 237 deletions packages/history/src/index.js
Original file line number Diff line number Diff line change
@@ -1,237 +1 @@

export default function history(Alpine) {
Alpine.magic('queryString', (el, { interceptor }) => {
let alias
let alwaysShow = false
let usePush = false

return interceptor((initialSeedValue, getter, setter, path, key) => {
let queryKey = alias || path

let { initial, replace, push, pop } = track(queryKey, initialSeedValue, alwaysShow)

setter(initial)

if (! usePush) {
console.log(getter())
Alpine.effect(() => replace(getter()))
} else {
Alpine.effect(() => push(getter()))

pop(async newValue => {
setter(newValue)

let tillTheEndOfTheMicrotaskQueue = () => Promise.resolve()

await tillTheEndOfTheMicrotaskQueue() // ...so that we preserve the internal lock...
})
}

return initial
}, func => {
func.alwaysShow = () => { alwaysShow = true; return func }
func.usePush = () => { usePush = true; return func }
func.as = key => { alias = key; return func }
})
})

Alpine.history = { track }
}

export function track(name, initialSeedValue, alwaysShow = false) {
let { has, get, set, remove } = queryStringUtils()

let url = new URL(window.location.href)
let isInitiallyPresentInUrl = has(url, name)
let initialValue = isInitiallyPresentInUrl ? get(url, name) : initialSeedValue
let initialValueMemo = JSON.stringify(initialValue)
let hasReturnedToInitialValue = (newValue) => JSON.stringify(newValue) === initialValueMemo

if (alwaysShow) url = set(url, name, initialValue)

replace(url, name, { value: initialValue })

let lock = false

let update = (strategy, newValue) => {
if (lock) return

let url = new URL(window.location.href)

if (! alwaysShow && ! isInitiallyPresentInUrl && hasReturnedToInitialValue(newValue)) {
url = remove(url, name)
} else {
url = set(url, name, newValue)
}

strategy(url, name, { value: newValue})
}

return {
initial: initialValue,

replace(newValue) { // Update via replaceState...
update(replace, newValue)
},

push(newValue) { // Update via pushState...
update(push, newValue)
},

pop(receiver) { // "popstate" handler...
window.addEventListener('popstate', (e) => {
if (! e.state || ! e.state.alpine) return

Object.entries(e.state.alpine).forEach(([iName, { value: newValue }]) => {
if (iName !== name) return

lock = true

// Allow the "receiver" to be an async function in case a non-syncronous
// operation (like an ajax) requests needs to happen while preserving
// the "locking" mechanism ("lock = true" in this case)...
let result = receiver(newValue)

if (result instanceof Promise) {
result.finally(() => lock = false)
} else {
lock = false
}
})
})
}
}
}

function replace(url, key, object) {
let state = window.history.state || {}

if (! state.alpine) state.alpine = {}

state.alpine[key] = unwrap(object)

window.history.replaceState(state, '', url.toString())
}

function push(url, key, object) {
let state = { alpine: {...window.history.state.alpine, ...{[key]: unwrap(object)}} }

window.history.pushState(state, '', url.toString())
}

function unwrap(object) {
return JSON.parse(JSON.stringify(object))
}

function queryStringUtils() {
return {
has(url, key) {
let search = url.search

if (! search) return false

let data = fromQueryString(search)

return Object.keys(data).includes(key)
},
get(url, key) {
let search = url.search

if (! search) return false

let data = fromQueryString(search)

return data[key]
},
set(url, key, value) {
let data = fromQueryString(url.search)

data[key] = value

url.search = toQueryString(data)

return url
},
remove(url, key) {
let data = fromQueryString(url.search)

delete data[key]

url.search = toQueryString(data)

return url
},
}
}

// This function converts JavaScript data to bracketed query string notation...
// { items: [['foo']] } -> "items[0][0]=foo"
function toQueryString(data) {
let isObjecty = (subject) => typeof subject === 'object' && subject !== null

let buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
Object.entries(data).forEach(([iKey, iValue]) => {
let key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`

if (! isObjecty(iValue)) {
entries[key] = encodeURIComponent(iValue)
.replaceAll('%20', '+') // Conform to RFC1738
} else {
entries = {...entries, ...buildQueryStringEntries(iValue, entries, key)}
}
})

return entries
}

let entries = buildQueryStringEntries(data)


return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join('&')
}

// This function converts bracketed query string notation back to JS data...
// "items[0][0]=foo" -> { items: [['foo']] }
function fromQueryString(search) {
search = search.replace('?', '')

if (search === '') return {}

let insertDotNotatedValueIntoData = (key, value, data) => {
let [first, second, ...rest] = key.split('.')

// We're at a leaf node, let's make the assigment...
if (! second) return data[key] = value

// This is where we fill in empty arrays/objects allong the way to the assigment...
if (data[first] === undefined) {
data[first] = isNaN(second) ? {} : []
}

// Keep deferring assignment until the full key is built up...
insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first])
}

let entries = search.split('&').map(i => i.split('='))

let data = {}

entries.forEach(([key, value]) => {
// Query string params don't always have values... (`?foo=`)
if (! value) return

value = decodeURIComponent(value.replaceAll('+', '%20'))

if (! key.includes('[')) {
data[key] = value
} else {
// Convert to dot notation because it's easier...
let dotNotatedKey = key.replaceAll('[', '.').replaceAll(']', '')

insertDotNotatedValueIntoData(dotNotatedKey, value, data)
}
})

return data
}

// This plugin has been moved into the livewire/livewire repository until it's more stable and ready to tag.
36 changes: 0 additions & 36 deletions packages/history/src/url.js

This file was deleted.

Loading