<!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>
* {
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%;
}
}
// 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 += ' ' + c + ' \n'; } tableCode += " \n"; } tableCode += "
";
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;
});