Last active
July 6, 2024 03:30
-
-
Save ChrisShank/fa77877602cb134e34169ee0c6316302 to your computer and use it in GitHub Desktop.
Self-modifying HTML file
This file contains hidden or 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
<head> | |
<!-- Forked from https://cristobal.space/note --> | |
<meta charset="UTF-8"> | |
<style> | |
body { | |
font-family: monospace; | |
margin: 12px; | |
} | |
img { | |
width: 100%; | |
margin: 12px 0px; | |
} | |
#editor { | |
margin-bottom: 12px; | |
} | |
#editor button { | |
font-style: italic; | |
} | |
#text { | |
width: 400px; | |
height: 100px; | |
border-radius: 4px; | |
resize: none; | |
margin-bottom: 12px; | |
padding: 6px; | |
} | |
#drop { | |
width: 400px; | |
height: 100px; | |
font-style: italic; | |
border: 1px dashed black; | |
border-radius: 4px; | |
margin-bottom: 12px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
#content { | |
width: 400px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="editor"> | |
<textarea id="text">Write HTML...</textarea> | |
<div id="drop">DROP IMAGES</div> | |
<button onclick="append()">Append</button> | |
<button onclick="save()">Save</button> | |
<button onclick="saveAs()">Save As</button> | |
</div> | |
<div id="content">Write HTML...Write HTML...Write HTML...Write HTML...Write HTML...</div> | |
<!-- Script for creating new --> | |
<script type="module"> | |
const noop = (e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
}; | |
const toBase64 = (blob) => | |
new Promise((resolve) => { | |
const reader = new FileReader(); | |
reader.onloadend = () => resolve(reader.result); | |
reader.readAsDataURL(blob); | |
}); | |
const content = document.getElementById('content'); | |
const textArea = document.getElementById('text'); | |
const dropArea = document.getElementById('drop'); | |
dropArea.addEventListener('dragenter', (e) => { | |
noop(e); | |
dropArea.style.border = '1px dotted black'; | |
}); | |
dropArea.addEventListener('drop', async (e) => { | |
noop(e); | |
dropArea.style.border = '1px dashed black'; | |
const img = document.createElement('img'); | |
const f = e.dataTransfer.files[0]; | |
const b64 = await toBase64(f); | |
img.src = b64; | |
content.appendChild(img); | |
}); | |
dropArea.addEventListener('dragover', noop); | |
dropArea.addEventListener('dragexit', noop); | |
window.append = () => { | |
content.innerHTML += textArea.value; | |
}; | |
</script> | |
<!-- Script for persist changes made to HTML file --> | |
<script type="module"> | |
// Based on https://github.com/jakearchibald/idb-keyval | |
class KeyValueStore { | |
#db; | |
#storeName; | |
constructor(name = 'keyval-store') { | |
this.#storeName = name; | |
const request = indexedDB.open(name); | |
request.onupgradeneeded = () => request.result.createObjectStore(name); | |
this.#db = new Promise((resolve, reject) => { | |
request.onsuccess = () => resolve(request.result); | |
request.onerror = () => reject(request.error); | |
}); | |
} | |
#promisifyTransaction(transaction) { | |
return new Promise((resolve, reject) => { | |
transaction.oncomplete = transaction.onsuccess = () => resolve(transaction.result); | |
transaction.onabort = transaction.onerror = () => reject(transaction.error); | |
}); | |
} | |
#getStore(mode) { | |
return this.#db.then((db) => | |
db.transaction(this.#storeName, mode).objectStore(this.#storeName) | |
); | |
} | |
get(key) { | |
return this.#getStore('readonly').then((store) => | |
this.#promisifyTransaction(store.get(key)) | |
); | |
} | |
set(key, value) { | |
return this.#getStore('readwrite').then((store) => { | |
store.put(value, key); | |
return this.#promisifyTransaction(store.transaction); | |
}); | |
} | |
clear() { | |
return this.#getStore('readwrite').then((store) => { | |
store.clear(); | |
return this.#promisifyTransaction(store.transaction); | |
}); | |
} | |
} | |
// Feature detection. The API needs to be supported | |
// and the app not run in an iframe. | |
const supportsFileSystemAccess = | |
'showSaveFilePicker' in window && | |
(() => { | |
try { | |
return window.self === window.top; | |
} catch { | |
return false; | |
} | |
})(); | |
class HTMLFileSaver { | |
#store = new KeyValueStore('html-file-saver'); | |
#file = this.#store.get('file'); | |
async save(content, promptNewFile = false) { | |
let file = await this.#file; | |
if (file === undefined || promptNewFile) { | |
this.#file = showSaveFilePicker({ | |
id: 'self-modifying_html_file', | |
suggestedName: 'index.html', | |
types: [{ description: 'HTML document', accept: { 'text/html': ['.html'] } }], | |
}); | |
file = await this.#file; | |
await this.#store.set('file', file); | |
} | |
const options = { mode: 'readwrite' }; | |
if ( | |
(await file.queryPermission(options)) !== 'granted' && | |
(await file.requestPermission(options)) !== 'granted' | |
) { | |
throw new Error('File write permission not granted.'); | |
} | |
const writer = await file.createWritable(); | |
await writer.write(content); | |
await writer.close(); | |
} | |
} | |
const saver = new HTMLFileSaver(); | |
window.save = () => saver.save(document.documentElement.innerHTML); | |
window.saveAs = () => saver.save(document.documentElement.innerHTML, true); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment