Skip to content
Open
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
305 changes: 305 additions & 0 deletions dwitter/static/js/drafts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
(function() {
var STORAGE_AUTOSAVE = 'dwitter:draft:current';
var STORAGE_DRAFTS = 'dwitter:drafts';
var AUTOSAVE_DEBOUNCE_MS = 500;

function readJson(key, fallback) {
try {
var raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch (e) {
return fallback;
}
}

function writeJson(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
// localStorage full or disabled - ignore
}
}

function removeKey(key) {
try {
localStorage.removeItem(key);
} catch (e) {
// ignore
}
}

function debounce(fn, ms) {
var timer = null;
return function() {
var args = arguments;
var ctx = this;
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(ctx, args);
}, ms);
};
}

function formatTimestamp(ms) {
return new Date(ms).toLocaleString();
}

function findForm(el) {
var node = el;
while (node && node.tagName !== 'FORM') {
node = node.parentNode;
}
return node;
}

// Programmatic value updates don't fire keyup, so dispatch one to reuse
// scrolling.js's existing handler (updates iframe preview + textarea size,
// and toggles submit-button enabled state).
function dispatchKeyup(el) {
el.dispatchEvent(new Event('keyup', { bubbles: true }));
}

function loadCodeIntoEditor(editor, code) {
editor.value = code;
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
dispatchKeyup(editor);
}

function makeId() {
return 'd' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}

function init() {
var editor = document.getElementById('editor');
if (!editor) return;

var statusEl = document.getElementById('draft-status');
var listEl = document.getElementById('drafts-list');
var saveButton = document.getElementById('draft-save-button');
var restorePrompt = document.getElementById('draft-restore-prompt');
var restoreTimeEl = restorePrompt && restorePrompt.querySelector('.draft-restore-time');
var restoreButton = document.getElementById('draft-restore-button');
var discardButton = document.getElementById('draft-discard-button');
if (!statusEl || !listEl || !saveButton || !restorePrompt
|| !restoreTimeEl || !restoreButton || !discardButton) {
return;
}

var defaultCode = editor.value;
var form = findForm(editor);

function setStatus(text) {
statusEl.textContent = text;
}

function hideRestorePrompt() {
restorePrompt.style.display = 'none';
}

function showRestorePrompt(savedAt) {
restoreTimeEl.textContent = formatTimestamp(savedAt);
restorePrompt.style.display = '';
}

function renameDraft(id, newName) {
var trimmed = (newName || '').trim();
if (!trimmed) return;
var current = readJson(STORAGE_DRAFTS, []);
var changed = false;
var next = current.map(function(d) {
if (d.id !== id || d.name === trimmed) return d;
changed = true;
return { id: d.id, name: trimmed, code: d.code, savedAt: d.savedAt };
});
if (!changed) return;
writeJson(STORAGE_DRAFTS, next);
renderDrafts();
}

function makeNameButton(draft) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'draft-name';
btn.textContent = draft.name;
btn.title = 'Click to rename';
btn.addEventListener('click', function() {
startRename(btn, draft);
});
return btn;
}

function startRename(nameEl, draft) {
var input = document.createElement('input');
input.type = 'text';
input.className = 'draft-name-input';
input.value = draft.name;
input.setAttribute('aria-label', 'Rename draft');

var settled = false;
function settle(applyRename) {
if (settled) return;
settled = true;
var trimmed = (input.value || '').trim();
if (applyRename && trimmed && trimmed !== draft.name) {
// Triggers a full re-render with the new name.
renameDraft(draft.id, trimmed);
return;
}
// No change — restore the name button in-place rather than
// re-rendering, so a click on another draft's name still lands.
if (input.parentNode) {
input.replaceWith(makeNameButton(draft));
}
}

input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
settle(true);
} else if (e.key === 'Escape') {
e.preventDefault();
settle(false);
}
});
input.addEventListener('blur', function() { settle(true); });

nameEl.replaceWith(input);
input.focus();
input.select();
}

function renderDrafts() {
var drafts = readJson(STORAGE_DRAFTS, []);
listEl.innerHTML = '';

if (!drafts.length) {
var empty = document.createElement('li');
empty.className = 'drafts-empty';
empty.textContent = 'No saved drafts yet.';
listEl.appendChild(empty);
return;
}

drafts.forEach(function(draft) {
var li = document.createElement('li');
li.className = 'draft-item';

var nameEl = makeNameButton(draft);

var ts = document.createElement('span');
ts.className = 'draft-timestamp';
ts.textContent = formatTimestamp(draft.savedAt);

var loadBtn = document.createElement('button');
loadBtn.type = 'button';
loadBtn.className = 'draft-load';
loadBtn.textContent = 'Load';
loadBtn.title = 'Load this draft into the editor';
loadBtn.addEventListener('click', function() {
loadCodeIntoEditor(editor, draft.code);
setStatus('Loaded "' + draft.name + '"');
});

var deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'draft-delete';
deleteBtn.textContent = 'x';
deleteBtn.title = 'Delete draft';
deleteBtn.addEventListener('click', function() {
if (!window.confirm('Delete draft "' + draft.name + '"?')) return;
var current = readJson(STORAGE_DRAFTS, []);
var next = current.filter(function(d) { return d.id !== draft.id; });
writeJson(STORAGE_DRAFTS, next);
renderDrafts();
});

li.appendChild(nameEl);
li.appendChild(ts);
li.appendChild(loadBtn);
li.appendChild(deleteBtn);
listEl.appendChild(li);
});
}

function saveAsDraft() {
var code = editor.value;
if (!code.trim()) {
setStatus('Nothing to save.');
return;
}

var savedAt = Date.now();
var draft = {
id: makeId(),
name: 'Untitled draft ' + formatTimestamp(savedAt),
code: code,
savedAt: savedAt,
};
var drafts = readJson(STORAGE_DRAFTS, []);
drafts.unshift(draft);
writeJson(STORAGE_DRAFTS, drafts);
setStatus('Saved as draft (click name to rename)');
renderDrafts();

// Open inline rename on the just-added item so the user can name it
// without breaking flow.
var firstItem = listEl.querySelector('.draft-item .draft-name');
if (firstItem) {
startRename(firstItem, draft);
}
}

var autoSave = debounce(function() {
var code = editor.value;
if (code === defaultCode) {
removeKey(STORAGE_AUTOSAVE);
setStatus('');
return;
}
writeJson(STORAGE_AUTOSAVE, { code: code, savedAt: Date.now() });
setStatus('Autosaved ' + new Date().toLocaleTimeString());
}, AUTOSAVE_DEBOUNCE_MS);

editor.addEventListener('input', function() {
// Once the user starts editing, the previous autosave is no longer
// "the thing they left behind" — it'll be overwritten as they type.
hideRestorePrompt();
autoSave();
});
saveButton.addEventListener('click', saveAsDraft);

restoreButton.addEventListener('click', function() {
var stored = readJson(STORAGE_AUTOSAVE, null);
if (!stored || !stored.code) return;
loadCodeIntoEditor(editor, stored.code);
hideRestorePrompt();
setStatus('Restored autosaved draft');
});

discardButton.addEventListener('click', function() {
removeKey(STORAGE_AUTOSAVE);
hideRestorePrompt();
setStatus('Autosaved draft discarded');
});

if (form) {
form.addEventListener('submit', function() {
removeKey(STORAGE_AUTOSAVE);
});
}

var stored = readJson(STORAGE_AUTOSAVE, null);
if (stored && stored.code && stored.code !== defaultCode) {
showRestorePrompt(stored.savedAt || Date.now());
}

renderDrafts();
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Loading