storable = (view_, keyOrOptions, options = {}) => {
validate(
view_ instanceof HTMLElement,
'View is not an HTMLElement. Did you forget to include the `viewof` keyword when passing to `storable`?'
)
const defaultKey = getNotebookKey()
const options_ = {
renderValue: inspect,
stringify: JSON.stringify,
parse: JSON.parse,
key: (typeof keyOrOptions === 'string') ? keyOrOptions : defaultKey,
...((typeof keyOrOptions === 'object') ? keyOrOptions : options),
}
;[
['renderValue', 'function'],
['stringify', 'function'],
['parse', 'function'],
['key', 'string']
].forEach(([key, type]) => validate(typeof options_[key] === type, `options.${key} must be a ${type}`))
const state = {
name: '',
currentList: [],
}
const syncToLocalStorage = () => {
localStorage.setItem(
options_.key,
JSON.stringify(state.currentList.map(([n, v], i, arr) => [n, options_.stringify(v, i, arr)]))
)
}
// Parse the list of saved configurations from localStorage and put in state.currentList
// This is the reverse process of syncToLocalStorage
const syncFromLocalStorage = () => {
const stringList = localStorage.getItem(options_.key)
if (!stringList) {
state.currentList = []
return
}
state.currentList = JSON.parse(stringList).map(([n, vstring], i, arr) => [n, options_.parse(vstring, i, arr)])
}
// Pull from localstorage and rerender the table body to match
const update = () => {
// Pull from localstorage
syncFromLocalStorage()
// Rerender the table body
const tbody = saveInterface.querySelector('tbody[data-currentlist]')
tbody.replaceChildren(...renderCurrentList())
}
// When the input field is updated, sync state.name
const handleNameInput = e => {
state.name = e.target.value
if (state.name !== '') {
saveInterface.querySelector('button[data-savebutton]').disabled = false
} else {
saveInterface.querySelector('button[data-savebutton]').disabled = true
}
}
// Add the current value to state.currentList, then push to localStorage
const handleSave = e => {
e.preventDefault()
// If the name field is blank, don't save
if (state.name == '') return
// Push the current config to the current list
state.currentList.push([state.name, view_.value])
// Write list to localstorage
syncToLocalStorage()
update()
// Clear the name field
saveInterface.querySelector('input[data-nameinput]').value = ''
state.name = ''
}
// Factory that generates a handler function which removes the item
// at index i from state.currentList and then pushes to localStorage
const handleDeleteFactory = i => {
return e => {
state.currentList.splice(i, 1)
syncToLocalStorage()
update()
}
}
// Factory that generates a handler function which backwrites
// the value at index i in state.currentList to the view
const handleApplyFactory = i => {
return e => {
e.preventDefault()
const [name, value] = state.currentList[i]
view_.value = value
view_.dispatchEvent(new Event('input'))
}
}
// Handler that listens for changes to the view and updates the "new item"
// row in the table. We don't get this automatically because `viewof view`,
// which is the first argument to `storable` doesn't trigger reevaluation
// when `view` changes in Observable
const handleViewInput = () => {
saveInterface.querySelector('td[data-currentvalue]').replaceChildren(
htl.html.fragment`${options_.renderValue(view_.value)}`
)
}
// We bind the listener immediately to the view
view_.addEventListener('input', handleViewInput)
// Template for the table rows
const renderCurrentList = () => state.currentList.map(([name, value], i, arr) => htl.html.fragment`
<tr>
<td>
<a href="#" onclick=${handleApplyFactory(i)} style="font-weight: bold;">
${name}
</a>
</td>
<td>${options_.renderValue(value, i, arr)}</td>
<td>
<button onclick=${handleDeleteFactory(i)} type="button">Delete</button>
</td>
</tr>
`)
// We need to sync from localStorage once before rendering
syncFromLocalStorage()
// Render the whole table!
const saveInterface = htl.html`
<form data-saveform onsubmit=${handleSave} action="" style="max-width: 100%; overflow: auto">
<table>
<thead>
<tr>
<th>Name <span style="font-weight: 400">(click to restore)</span></th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody data-currentlist>
${renderCurrentList()}
</tbody>
<tfoot>
<tr style="border-top: 1px solid #CCC; border-bottom: 1px solid #CCC;">
<td style="padding-top: 3px">
<label for=${DOM.uid(`name-${options_.key}`)} style="display: block; height: 0; width: 0; overflow: hidden;">Name</label>
<input id=${DOM.uid(`name-${options_.key}`)} data-nameinput oninput=${handleNameInput} type="text" placeholder="Name" style="max-width: 80px" />
</td>
<td style="padding-top: 3px" data-currentvalue>
${options_.renderValue(view_.value)}
</td>
<td style="padding-top: 3px">
<button data-savebutton disabled=${true} type="submit">Save</button>
</td>
</tr>
</tfoot>
</table>
</form>
`
return saveInterface
}