Content Creators Draft Save
The Browser-Based Revision History & Recovery System provides real-time autosave, manual save points, version recovery, and local browser storage support for web-based editors.
Browser-Based Revision History & Recovery System
Designed for article creators, developers, and content managers, this system helps prevent data loss, improves editing workflow efficiency, and allows users to restore or manage previous revisions directly from the editor without page reloads or server-side processing.
[Browser Session Storage - Features]
  1. Word Count: Right-click in the textarea and while typing.
  2. Character Count: displays count on page load and during typing.
  3. Show/Hide the Storage List
  4. Time of each Auto Save
  5. Storage usage and capacity
  6. Self Save option
  7. Delete each Entry
  8. Delete all entries
  9. Will not save unless something is edited.
    If you leave the page open and no work is done, the last save will be from your previous edit.
Live Template Example - Browser Session Storage
Changes you will have to make.
  1. Line 11: Form Name
  2. Line 12: Textarea Name
  3. Line 30: [pageid] This is where you add in your QueryString information. Change to fit your site's needs.
  4. Line 59: Change this to the time you want to Auto-Save. (These are in milliseconds)
    60000 = 1 minute
    120000 = 2 minutes (This is my default setting)
    180000 = 3 minutes
    240000 = 4 minutes
    300000 = 5 minutes (This would be the longest I would go)
  5. Line 129: [formValues]
    These have to match the elements you want to save to storage. For this demo, it is the Title and Textarea. Add all fields to this area by their ID.
  6. Line 246: [fieldMap](Restore) Goes with line 131 above. Whatever you add to the above, you will need to add here.
[HTML]
CFFCS | CarrzSynEdit: | HTML (Hyper Text Markup Language)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Editor with History</title>
<link rel="stylesheet" href="<%=siteurl%>Assets/Templates_/5/History.css">
</head>
<body>

<div id="historyWordEditorShell">

    <div class="history-editor-split">

     <section class="history-editor-panel" style="height: 460px !important;"><!-- Height is for Demo only! -->
<button type="button" class="history-primary-save-btn" id="btnTogglePanels" onclick="toggleHistory()">Show History</button>
<form id="EditorForm"> 
Title: <input name="txtName" type="text" value="" id="txtName" class="history-title">
 <div class="history-panel-title">
   <span>History Editor</span>
  </div>
<!-- Height is for Demo only! -->
<textarea id="txtEditor" namd="txtEditor" class="history-textarea" style="height: 250px !important;">
Just add some text and click the [Show History] button on top, then click [Save] Version to save to history.
Or wait for "1 minute" and it will auto-save itself.
</textarea>

  <div class="history-editor-status-bar">
   <span id="historyStatusText">Ready</span>
   <span id="historyWordCount">Words: 0</span>
  
Chars Count: Typed <span id="charCount">0</span> | Remaining: <span id="charsLeft">0</span>

     <div id="statusContainer" style="font-size: 12px; color: #666;">
    <span id="saveIndicator"></span>
    <span id="autoSaveMsg">Ready</span>
</div>

</div>
<div class="progress-container">

    <div id="meterFill"></div>
    <!-- The text overlay -->
    <span id="storageText">
     0 KB / 5,120 KB Used
    </span>
</div>
</form>

     </section><!--history-editor-panel-->

<section class="history-editor-panel">

	    <div id="historyPanelHolder" >
    <div class="column-history">
     <button class="btn-green" type="button" onclick="saveToHistory(false)">[Save] Version</button>
    <div id="historyPanel">
     <span class="history-label">Revision History</span>
     <div id="historyList"></div>
		
		
    <!-- New Danger Zone -->
    <div id="WipeAllData">
     <button class="btn-red" onclick="requestWipe()">WIPE ALL DATA</button>
    </div>

    </div>
	
<div id="confirmBar">
    <span id="RestoreVersion">Restore this version? This will replace your current text.</span>
    <div>
     <button class="btn-green" type="button" onclick="executeRestore()">Confirm Restore</button>
     <button class="btn-red" type="button"onclick="cancelRestore()">Cancel</button>
    </div>
</div>	<!--confirmBar-->

    </div> <!--column-history-->
    </div> <!--historyPanelHolder-->

 </section> <!-- history-editor-panel -->

    </div>

</div>

<script src="<%=siteurl%>Assets/Templates_/5/History.js"></script>
</body>
</html>

[CSS]
CFFCS | CarrzSynEdit: | CSS (Cascading Style Sheets)
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 20px;
  background: #f3f5f8;
  font-family: Segoe UI, Arial, sans-serif;
  color: #111827;
}

.history-editor-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(360px, 0.85fr);
  gap: 18px;
  align-items: start;
}

.history-editor-panel {
  background: #fff;
  border: 1px solid #d6dce8;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 3px 14px rgba(15, 23, 42, 0.08);
}

.history-panel-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-bottom: 1px solid #d6dce8;
  background: linear-gradient(#fff, #f7f8fb);
  font-weight: 700;
}

.history-panel-title-btn {
  border: 1px solid #cbd5e1;
  background: #fff;
  border-radius: 6px;
  padding: 6px 10px;
  cursor: pointer;
}

.history-textarea {
  width: 100%;
  min-height: 250px;
  resize: vertical;
  border: 0;
  outline: none;
  padding: 18px;
  overflow: auto;
  font: 15px/1.7 Consolas, "Courier New", monospace;
}

.history-title {
  width: 100%;
  min-height: 38px;
  box-sizing: border-box;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  padding: 8px 10px;
  font-family: Segoe UI, Arial, Helvetica, sans-serif;
  font-size: 14px;
  background: #ffffff;
}

.history-title:focus {
  outline: 2px solid rgba(37, 99, 235, 0.18);
  border-color: #7aa2e8;
}


#historyPanelHolder {
  display: none;
}

#shortcutPanel {
  display: block;
}

.column-history {
  flex: 1;
  min-width: 250px;
  background: #f9f9f9;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 8px;

  /* --- THE FIXES --- */
  position: -webkit-sticky; /* For Safari support */
  position: sticky;
  top: 120px; /* Threshold required */
  align-self: flex-start; /* Stops the column from stretching to full height */
}
#historyList {
  height: 300px; /* Required: set a fixed height */
  overflow-y: auto; /* Adds scrollbar only if content overflows */
  overflow-x: hidden; /* Disables horizontal scroll */
}

.history-editor-status-bar {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 12px;
  padding: 9px 14px;
  background: #f8fafc;
  border: 1px solid var(--history-editor-border);
  color: var(--history-editor-muted);
  font-size: 13px;
  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
}

.progress-container {
  position: relative;
  height: 20px;
  background: #eee;
  border-radius: 5px;
  overflow: hidden;
  border: 1px solid #ccc;
}
#meterFill {
  height: 100%;
  width: 0%;
  background: #28a745;
  transition: width 0.3s ease;
}
#storageText {
  position: absolute;
  width: 100%;
  top: 0;
  left: 0;
  text-align: center;
  line-height: 20px;
  font-size: 11px;
  font-weight: bold;
  color: #000;
  pointer-events: none;
}
#confirmBar {
  display: none;
  background: #fff3cd;
  border: 1px solid #ffeeba;
  padding: 10px;
  margin-bottom: 10px;
  border-radius: 6px;
  justify-content: space-between;
  align-items: center;
  border-left: 5px solid #ffc107;
}
#RestoreVersion {
  font-size: 14px;
  color: #856404;
  font-weight: bold;
}
.btn-green {
  padding: 5px 12px;
  margin-right: 5px;
  font-size: 12px;
}

.btn-red {
  padding: 5px 12px;
  font-size: 12px;
}
#WipeAllData {
  margin-top: 20px;
  border-top: 2px solid #eee;
  padding-top: 15px;
}


.history-primary-save-btn {
  border: 1px solid #1d4ed8;
  border-radius: 4px;
  height: 20px !important;
  color: #121212;
  padding: 1px !important;
  cursor: pointer;
  font-size: 12.5px;
  box-shadow: 0 4px 14px rgba(37, 99, 235, 0.25);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  width: auto;
  line-height: 1;
}

@media (max-width: 900px) {
  .history-editor-split {
  grid-template-columns: 1fr;
  }
}

/* Mobile Tweak: Make both columns 100% wide on small screens */
@media (max-width: 768px) {
  .column-history {
  flex: 1 1 100%;
  }
}

[JavaScript]
CFFCS | CarrzSynEdit: | JS (JavaScript)
// 1. GLOBAL VARIABLES
let storageKey;
let editor;
let lastSavedContent = ""; 
let pendingRestoreIndex = null;
let isWipeMode = false;
let pendingDeleteIndex = null;
const storageLimit = 5 * 1024 * 1024; // 5MB Estimate


var EditorForm = document.getElementById('EditorForm');


function refreshHistoryStatus() {

    if (typeof updateStats === 'function') {
        updateStats();
    }

    if (typeof renderHistory === 'function') {
        renderHistory();
    }
}


// 2. SETUP & INITIALIZATION
document.addEventListener('DOMContentLoaded', () => {
    editor = document.getElementById('txtEditor');
    
    const params = new URLSearchParams(window.location.search);
    const pageId =
    params.get('ArticleID') ||
    params.get('catid') ||
    params.get('CatID') ||
    'default';
	const pathKey =
    window.location.hostname +
    window.location.pathname;
    storageKey = bbcode_history_db_${pathKey}_ID_${pageId}
Copy
Search Site
Search Google
; if (editor) { editor.addEventListener('input', updateStats); editor.addEventListener('focus', refreshHistoryStatus); editor.addEventListener('click', refreshHistoryStatus); updateStats(); renderHistory(); } renderHistory(); setInterval(() => { if (editor && editor.value.trim().length > 0) { saveToHistory(true); } }, 120000); }); // 3. CORE FUNCTIONS function updateStats() { // 1. Ensure the editor and storageKey exist if (!editor || !storageKey) return; // 2. Main Editor Length & Remaining Space (Numbers only) const editorLen = editor.value.length; const maxLimit = 400000; // Character limit for the textarea, this can be change to your setup. This will show in the Remaining Section in the bottom Status Bar. const remaining = maxLimit - editorLen; // 3. STORAGE CALCULATION (The 5MB Limit) const historyData = localStorage.getItem(storageKey) || ""; const storageUsedBytes = historyData.length; const storageLimitBytes = 5 * 1024 * 1024; // 5MB in bytes // Convert to KB for the display label const usedKB = Math.round(storageUsedBytes / 1024); const limitKB = 5120; // 5MB in KB // 4. Target the display elements const charCountEl = document.getElementById('charCount'); const charsLeftEl = document.getElementById('charsLeft'); const meterFillEl = document.getElementById('meterFill'); const storageTextEl = document.getElementById('storageText'); // The text on the bar // 5. Update the text values (Character Count) if (charCountEl) { charCountEl.innerText = editorLen.toLocaleString(); } if (charsLeftEl) { charsLeftEl.innerText = (remaining > 0 ? remaining : 0).toLocaleString(); } // 6. Update the text ON the bar (Storage Used) if (storageTextEl) { storageTextEl.innerText = ${<span class="JSPeriodLeft">usedKB</span>.<span class="JSPeriodRight">toLocaleString</span>()} KB / ${<span class="JSPeriodLeft">limitKB</span>.<span class="JSPeriodRight">toLocaleString</span>()} KB Used
Copy
Search Site
Search Google
; } // 7. Update the progress bar (Storage Used) if (meterFillEl) { const percent = (storageUsedBytes / storageLimitBytes) * 100; meterFillEl.style.width = Math.min(percent, 100) + "%"; // Color logic based on Storage: Green < 50%, Yellow 50-80%, Red > 80% if (percent > 80) { meterFillEl.style.backgroundColor = "#dc3545"; // Red } else if (percent > 50) { meterFillEl.style.backgroundColor = "#ffc107"; // Yellow } else { meterFillEl.style.backgroundColor = "#28a745"; // Green } } updateWordCount(); } function saveToHistory(isAuto = false) { // const form = document.querySelector('EditorForm'); const saveBtn = document.getElementById('btn-green'); // Targeted your specific ID // 1. Safety check: does the form exist? if (!EditorForm) { console.error("EditorForm not found!"); return; } // Define tracked fields (Option 2: Specific Manual List) const formValues = { title: document.getElementById('txtName').value, editor: document.getElementById('txtEditor').value, // meta: document.getElementById('"txtMetaKeys'), // keys: document.getElementById('"txtMetaDesc') }; // Convert to string just for the comparison check const currentSnapshot = JSON.stringify(formValues); if (currentSnapshot === lastSavedContent) return; let history = JSON.parse(localStorage.getItem(storageKey)) || []; const timestamp = new Date().toLocaleTimeString(); lastSavedContent = currentSnapshot; history.unshift({ time: new Date().toLocaleString(), content: formValues, // Saves {title: "...", editor: "..."} type: isAuto ? "Auto" : "Manual" }); if (history.length > 30) history.pop(); localStorage.setItem(storageKey, JSON.stringify(history)); // 2. ONLY change the button text if it's a Manual save if (!isAuto && saveBtn) { saveBtn.innerText = "Manually Saved!"; saveBtn.style.backgroundColor = "#218838"; // Optional: darker green feedback // 3. Reset the button text back to normal after 3 seconds setTimeout(() => { saveBtn.innerText = "[Save] Version"; saveBtn.style.backgroundColor = ""; // Resets to original CSS color }, 3000); } renderHistory(); updateStats(); // --- TRIGGER THE INDICATOR --- const indicator = document.getElementById('saveIndicator'); if (indicator) { indicator.classList.remove('pulse'); void indicator.offsetWidth; // Trigger reflow to restart animation indicator.classList.add('pulse'); } const msgEl = document.getElementById('autoSaveMsg'); if (msgEl) { msgEl.innerText = (isAuto ? "Auto-Saved: " : "Manually Saved: ") + timestamp; } const panel = document.getElementById('historyPanel'); if (panel.style.display === 'block') { panel.style.borderColor = "#28a745"; setTimeout(() => { panel.style.borderColor = "#ddd"; }, 500); } } function executeRestore() { if (pendingDeleteIndex !== null) { executeDeleteHistoryPoint(); return; } if (!EditorForm) return; if (isWipeMode) { // 1. DELETE ONLY THE HISTORY DATA localStorage.removeItem(storageKey); // 2. RESET THE "LAST SAVED" TRACKER // This ensures the next auto-save creates a fresh history entry lastSavedContent = ""; // 3. REFRESH THE UI renderHistory(); cancelRestore(); // 4. FEEDBACK const msgEl = document.getElementById('autoSaveMsg'); if (msgEl) msgEl.innerText = "History Wiped (Editor content preserved)"; } else if (pendingRestoreIndex !== null) { // --- RESTORE LOGIC (KEEPS WORKING AS IS) --- const history = JSON.parse(localStorage.getItem(storageKey)); if (!history || !history[pendingRestoreIndex]) return; const savedData = history[pendingRestoreIndex].content; const fieldMap = { 'txtName': savedData.title, 'txtEditor': savedData.editor // 'Meta': savedData.meta, // 'Keys': savedData.keys }; Object.keys(fieldMap).forEach(name => { const input = EditorForm.querySelector([name=<span class="JSDoubleQuotes">"${name}"</span>]
Copy
Search Site
Search Google
); if (input) input.value = fieldMap[name] || ""; }); if (editor) editor.value = savedData.editor || ""; lastSavedContent = JSON.stringify(savedData); updateStats(); cancelRestore(); } } // ... Keep your renderHistory, cancelRestore, toggleHistory, and insertTable as they were ... function renderHistory() { const list = document.getElementById('historyList'); if (!list) return; const history = JSON.parse(localStorage.getItem(storageKey)) || []; list.innerHTML = ""; if (history.length === 0) { list.innerHTML = "<div style='color:#999; font-size:12px;'>No saves yet.</div>"; return; } history.forEach((entry, index) => { const item = document.createElement('div'); item.className = 'history-item'; const color = entry.type === "Manual" ? "#007bff" : "#666"; item.innerHTML = ${<span class="JSPeriodLeft">entry</span>.<span class="JSPeriodRight">type</span>}
${<span class="JSPeriodLeft">entry</span>.<span class="JSPeriodRight">time</span>}
Copy
Search Site
Search Google
; item.onclick = () => restoreVersion(index); item.onclick = () => restoreVersion(index); list.appendChild(item); }); } function restoreVersion(index) { console.log("Restoring version at index:", index); // Debug line pendingRestoreIndex = index; const bar = document.getElementById('confirmBar'); if (bar) bar.style.display = "flex"; } function cancelRestore() { document.getElementById('confirmBar').style.display = "none"; document.getElementById('confirmBar') .querySelector('span') .innerText = "Restore this version?"; var confirmBtn = document.querySelector('#confirmBar .btn-green'); if (confirmBtn) { confirmBtn.innerText = "Confirm Restore"; } pendingRestoreIndex = null; pendingDeleteIndex = null; isWipeMode = false; } var btnTogglePanels = document.getElementById('btnTogglePanels'); function toggleHistory() { const panel = document.getElementById('historyPanelHolder'); if (!panel) return; // 1. Check current visible state (handling empty string default) const isCurrentlyVisible = (panel.style.display === 'block'); if (isCurrentlyVisible) { // Hide it panel.style.display = 'none'; if (btnTogglePanels) btnTogglePanels.textContent = 'Show History'; } else { // Show it panel.style.display = 'block'; if (btnTogglePanels) btnTogglePanels.textContent = 'Hide History'; renderHistory(); } } function insertTable() { if (!editor) return; const rows = parseInt(document.getElementById('rows').value) || 1; const cols = parseInt(document.getElementById('cols').value) || 1; let tableCode = "\n"; for (let r = 0; r < rows; r++) { tableCode += '\n"; for (let c = 0; c < cols; c++) { tableCode += ' \n'; } tableCode += " \n"; } tableCode += "
' + c + '
"
; const start = editor.selectionStart; const end = editor.selectionEnd; editor.value = editor.value.substring(0, start) + tableCode + editor.value.substring(end); editor.focus(); } function requestWipe() { isWipeMode = true; pendingRestoreIndex = null; const bar = document.getElementById('confirmBar'); bar.querySelector('span').innerText = "WIPE EVERYTHING? This deletes current text and all history."; bar.style.display = "flex"; } function deleteHistoryPoint(index) { pendingDeleteIndex = index; pendingRestoreIndex = null; isWipeMode = false; var bar = document.getElementById('confirmBar'); var confirmBtn = bar.querySelector('.btn-green'); if (confirmBtn) { confirmBtn.innerText = "Confirm Delete"; } if (!bar) { return; } var msg = bar.querySelector('span'); if (msg) { msg.innerText = "Delete this history point?"; } bar.style.display = "flex"; } function executeDeleteHistoryPoint() { if (pendingDeleteIndex === null) { return; } var history = JSON.parse(localStorage.getItem(storageKey)) || []; if (!history[pendingDeleteIndex]) { cancelRestore(); return; } history.splice(pendingDeleteIndex, 1); localStorage.setItem(storageKey, JSON.stringify(history)); pendingDeleteIndex = null; renderHistory(); if (typeof updateStats === 'function') { updateStats(); } var msgEl = document.getElementById('autoSaveMsg'); if (msgEl) { msgEl.innerText = "History point deleted."; } cancelRestore(); } // Select the DOM elements const textarea = document.getElementById('txtEditor'); const wordCountSpan = document.getElementById('historyWordCount'); // Listen for typing, pasting, or deleting text textarea.addEventListener('input', () => { // 1. Remove leading and trailing whitespace const text = textarea.value.trim(); // 2. If the textarea is empty, the count is exactly 0 if (text === ') { wordCountSpan.textContent = 0; return; } // 3. Split the text by any length of spaces, tabs, or newlines const words = text.split(/\s+/); // 4. Update the UI with the total array length wordCountSpan.textContent = words.length; });