Dev Notes WordPress Plugin
A clean, powerful WordPress plugin for writing and managing developer notes. Admin-only access with rich text editing and raw HTML view.


✨ Features
- Rich Text Editor – WYSIWYG with formatting toolbar
- Smart Saving – Auto-save + manual save with visual feedback
- Tables & Images – Insert tables and images from WordPress media library
- Dual Views – Visual editor + Raw HTML view with CodeMirror syntax highlighting
- Keyboard Shortcuts – Ctrl+S (PC) / ⌘+S (Mac) to save
- Security – Admin-only access with proper WordPress security
You can copy entire code snippet in Scripts Organizer
Settings:
- Everywhere
- Plugins Loaded

<?php
// Add menu item under "Tools", visible only to administrators
add_action('admin_menu', function () {
if (current_user_can('manage_options')) {
add_management_page(
'Dev Notes',
'Dev Notes',
'manage_options',
'dev-notes',
'devnotes_render_page'
);
}
});
// Enqueue scripts and styles for the admin page
add_action('admin_enqueue_scripts', function($hook) {
// Only load on our specific page
if ($hook !== 'tools_page_dev-notes') {
return;
}
// Enqueue media library for image uploads
wp_enqueue_media();
// Enqueue CodeMirror for raw view
$editor_settings = wp_enqueue_code_editor(array('type' => 'text/html'));
// Pass CodeMirror settings to JavaScript
wp_add_inline_script('wp-codemirror', 'window.devnotesCodeMirrorSettings = ' . wp_json_encode($editor_settings) . ';');
// Pass data to JavaScript
wp_localize_script('jquery', 'devnotesData', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('devnotes_save')
));
});
// AJAX handler for auto-save
add_action('wp_ajax_save_dev_notes', function() {
// Security checks
if (!check_ajax_referer('devnotes_save', 'devnotes_nonce', false) ||
!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
return;
}
if (isset($_POST['notes_content'])) {
update_option('devnotes_data', wp_unslash($_POST['notes_content']));
wp_send_json_success('Notes saved');
} else {
wp_send_json_error('No content provided');
}
});
// Render the plugin's settings page
function devnotes_render_page() {
// Security check
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.'));
}
$saved_data = get_option('devnotes_data', '');
?>
<div class="wrap">
<div style="display: flex; align-items: center; justify-content: space-between;">
<h1>Dev Notes</h1>
<div style="margin: 10px 0; display: flex; align-items: center;">
<span id="auto-save-status" style="margin-left: 15px; color: #666;"></span>
<button type="button" id="save-button" class="button button-primary" disabled>Save Notes</button>
</div>
</div>
<div style="margin: 20px 0;">
<div id="formatting-toolbar" style="margin-bottom: 10px; padding: 10px; background: #f1f1f1; border: 1px solid #ddd; border-radius: 4px 4px 0 0;">
<button type="button" class="format-btn" data-command="bold" title="Bold">
<strong>B</strong>
</button>
<button type="button" class="format-btn" data-command="italic" title="Italic">
<em>I</em>
</button>
<button type="button" class="format-btn" data-command="underline" title="Underline">
<u>U</u>
</button>
<span style="margin: 0 10px; color: #ccc;">|</span>
<button type="button" class="format-btn" data-command="insertUnorderedList" title="Bullet List">
• List
</button>
<button type="button" class="format-btn" data-command="insertOrderedList" title="Numbered List">
1. List
</button>
<span style="margin: 0 10px; color: #ccc;">|</span>
<button type="button" class="format-btn" data-command="formatBlock" data-value="h1" title="Heading 1">
H1
</button>
<button type="button" class="format-btn" data-command="formatBlock" data-value="h2" title="Heading 2">
H2
</button>
<button type="button" class="format-btn" data-command="formatBlock" data-value="p" title="Paragraph">
P
</button>
<span style="margin: 0 10px; color: #ccc;">|</span>
<button type="button" class="format-btn" id="insert-table-btn" title="Insert Table">
📋 Table
</button>
<button type="button" class="format-btn" id="insert-image-btn" title="Insert Image">
🖼️ Image
</button>
<span style="margin: 0 10px; color: #ccc;">|</span>
<button type="button" class="format-btn" id="toggle-view-btn" title="Toggle Raw View">
</> Raw
</button>
</div>
<div
id="devnotes-editor"
contenteditable="true"
style="
min-height: 400px;
padding: 20px;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
background: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
outline: none;
overflow-y: auto;
"
><?php echo $saved_data; ?></div>
<div id="devnotes-codemirror-container" style="display: none;">
<textarea
id="devnotes-code-preview"
readonly
style="
min-height: 400px;
width: 100%;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 13px;
outline: none;
resize: none;
"
></textarea>
</div>
</div>
</div>
<style>
.format-btn {
background: white;
border: 1px solid #ccc;
padding: 5px 10px;
margin-right: 5px;
cursor: pointer;
border-radius: 3px;
font-size: 12px;
}
.format-btn:hover {
background: #f8f8f8;
border-color: #999;
}
.format-btn:active,
.format-btn.active {
background: #007cba;
color: white;
border-color: #005a87;
}
#devnotes-editor:focus {
border-color: #007cba;
box-shadow: 0 0 0 1px #007cba;
}
#toggle-view-btn.active {
background: #007cba !important;
color: white !important;
border-color: #005a87 !important;
}
#save-button:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
.auto-save-error {
color: #d63638 !important;
}
#devnotes-editor h1 {
font-size: 2em;
font-weight: bold;
margin: 0.67em 0;
}
#devnotes-editor h2 {
font-size: 1.5em;
font-weight: bold;
margin: 0.75em 0;
}
#devnotes-editor p {
margin: 1em 0;
}
#devnotes-editor ul, #devnotes-editor ol {
margin: 1em 0;
padding-left: 2em;
}
ul {
list-style: disc;
}
#devnotes-editor table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
border: 1px solid #ddd;
}
#devnotes-editor table td, #devnotes-editor table th {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: left;
}
#devnotes-editor table th {
background-color: #f5f5f5;
font-weight: bold;
}
#devnotes-editor table tr:nth-child(even) {
background-color: #f9f9f9;
}
#devnotes-editor img {
max-width: 100%;
height: auto;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* CodeMirror customization */
.CodeMirror {
font-family: 'Monaco', 'Consolas', 'Courier New', monospace !important;
font-size: 13px !important;
background: #f8f9fa !important;
}
.CodeMirror-gutters {
background: #f1f3f4 !important;
border-right: 1px solid #ddd !important;
}
.CodeMirror-linenumber {
color: #666 !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const editor = document.getElementById('devnotes-editor');
const codePreview = document.getElementById('devnotes-code-preview');
const codeContainer = document.getElementById('devnotes-codemirror-container');
const toggleBtn = document.getElementById('toggle-view-btn');
const saveButton = document.getElementById('save-button');
const statusElement = document.getElementById('auto-save-status');
let hasUnsavedChanges = false;
let autoSaveTimeout;
let isCodeView = false;
let codeMirrorInstance = null;
// Formatting buttons
document.querySelectorAll('.format-btn').forEach(btn => {
btn.addEventListener('click', function() {
const command = this.dataset.command;
const value = this.dataset.value || null;
document.execCommand(command, false, value);
editor.focus();
markAsChanged();
});
});
// Table insertion
document.getElementById('insert-table-btn').addEventListener('click', function() {
insertTable();
markAsChanged();
});
// Image insertion
document.getElementById('insert-image-btn').addEventListener('click', function() {
openMediaLibrary();
});
// View toggle
toggleBtn.addEventListener('click', function() {
toggleView();
});
function insertTable() {
const rows = prompt('Number of rows:', '3') || '3';
const cols = prompt('Number of columns:', '3') || '3';
if (rows && cols) {
let tableHTML = '<table>';
// Create header row
tableHTML += '<tr>';
for (let j = 0; j < parseInt(cols); j++) {
tableHTML += '<th>Header ' + (j + 1) + '</th>';
}
tableHTML += '</tr>';
// Create data rows
for (let i = 1; i < parseInt(rows); i++) {
tableHTML += '<tr>';
for (let j = 0; j < parseInt(cols); j++) {
tableHTML += '<td>Cell ' + i + ',' + (j + 1) + '</td>';
}
tableHTML += '</tr>';
}
tableHTML += '</table><p></p>';
// Insert at current cursor position
document.execCommand('insertHTML', false, tableHTML);
editor.focus();
}
}
function openMediaLibrary() {
// Check if wp.media is available
if (typeof wp !== 'undefined' && wp.media) {
const mediaUploader = wp.media({
title: 'Choose Image',
button: {
text: 'Use This Image'
},
multiple: false,
library: {
type: 'image'
}
});
mediaUploader.on('select', function() {
const attachment = mediaUploader.state().get('selection').first().toJSON();
const imageHTML = '<img src="' + attachment.url + '" alt="' + (attachment.alt || attachment.title || 'Image') + '" /><p></p>';
// Only insert in visual mode, switch to visual if needed
if (isCodeView) {
toggleView(); // Switch back to visual
}
// Insert at current cursor position in visual editor
document.execCommand('insertHTML', false, imageHTML);
editor.focus();
markAsChanged();
});
mediaUploader.open();
} else {
alert('Media library not available. Please upload images through WordPress Media section.');
}
}
function formatHTML(html) {
// Simple HTML formatter to make it readable
let formatted = html;
// Add line breaks before opening tags
formatted = formatted.replace(/</g, '\n<');
// Clean up extra whitespace
formatted = formatted.replace(/\n\s*\n/g, '\n');
formatted = formatted.replace(/^\n/, '');
// Split into lines for indentation
const lines = formatted.split('\n');
let indentLevel = 0;
const indentSize = 2;
const formattedLines = lines.map(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return '';
// Decrease indent for closing tags
if (trimmedLine.startsWith('</')) {
indentLevel = Math.max(0, indentLevel - 1);
}
const indentedLine = ' '.repeat(indentLevel * indentSize) + trimmedLine;
// Increase indent for opening tags (but not self-closing or closing tags)
if (trimmedLine.startsWith('<') &&
!trimmedLine.startsWith('</') &&
!trimmedLine.endsWith('/>') &&
!trimmedLine.match(/<(br|hr|img|input|meta|link)\b[^>]*>/i)) {
indentLevel++;
}
return indentedLine;
});
return formattedLines.filter(line => line.trim()).join('\n');
}
function toggleView() {
if (isCodeView) {
// Switch to visual view
editor.style.display = 'block';
codeContainer.style.display = 'none';
toggleBtn.classList.remove('active');
toggleBtn.innerHTML = '</> Raw';
isCodeView = false;
// Enable formatting buttons
document.querySelectorAll('.format-btn:not(#toggle-view-btn)').forEach(btn => {
btn.disabled = false;
btn.style.opacity = '1';
});
editor.focus();
} else {
// Switch to raw preview with CodeMirror
const formattedHTML = formatHTML(editor.innerHTML);
codePreview.value = formattedHTML;
editor.style.display = 'none';
codeContainer.style.display = 'block';
toggleBtn.classList.add('active');
toggleBtn.innerHTML = '👁️ Visual';
isCodeView = true;
// Initialize CodeMirror if not already done
if (!codeMirrorInstance) {
// Debug: Check what's available
console.log('Checking CodeMirror availability:', {
wp: typeof wp,
wpCodeEditor: typeof wp !== 'undefined' ? typeof wp.codeEditor : 'undefined',
settings: typeof window.devnotesCodeMirrorSettings
});
if (typeof wp !== 'undefined' && wp.codeEditor && window.devnotesCodeMirrorSettings) {
try {
// Use WordPress CodeMirror API
const settings = Object.assign({}, window.devnotesCodeMirrorSettings, {
codemirror: Object.assign({}, window.devnotesCodeMirrorSettings.codemirror, {
readOnly: true,
lineNumbers: true,
lineWrapping: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
mode: 'htmlmixed'
})
});
codeMirrorInstance = wp.codeEditor.initialize(codePreview, settings);
// Set custom styling
codeMirrorInstance.codemirror.getWrapperElement().style.border = '1px solid #ddd';
codeMirrorInstance.codemirror.getWrapperElement().style.borderTop = 'none';
codeMirrorInstance.codemirror.getWrapperElement().style.borderRadius = '0 0 4px 4px';
codeMirrorInstance.codemirror.setSize(null, '400px');
console.log('CodeMirror initialized successfully');
} catch (error) {
console.error('Error initializing CodeMirror:', error);
codeMirrorInstance = null;
}
}
if (!codeMirrorInstance) {
// Fallback if CodeMirror is not available
console.warn('WordPress CodeMirror not available, using styled textarea fallback');
codePreview.style.display = 'block';
codePreview.style.height = '400px';
codePreview.style.padding = '20px';
codePreview.style.background = '#f8f9fa';
codePreview.style.fontFamily = 'Monaco, Consolas, "Courier New", monospace';
codePreview.style.fontSize = '13px';
codePreview.style.lineHeight = '1.4';
codePreview.style.border = '1px solid #ddd';
codePreview.style.borderTop = 'none';
codePreview.style.borderRadius = '0 0 4px 4px';
codePreview.style.resize = 'vertical';
}
} else if (codeMirrorInstance && codeMirrorInstance.codemirror) {
// Update content in existing CodeMirror instance
const formattedHTML = formatHTML(editor.innerHTML);
codeMirrorInstance.codemirror.setValue(formattedHTML);
codeMirrorInstance.codemirror.refresh();
} else {
// Update plain textarea fallback
const formattedHTML = formatHTML(editor.innerHTML);
codePreview.value = formattedHTML;
}
// Disable formatting buttons (except toggle)
document.querySelectorAll('.format-btn:not(#toggle-view-btn)').forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.5';
});
}
}
function markAsChanged() {
if (!hasUnsavedChanges) {
hasUnsavedChanges = true;
updateSaveButton();
}
triggerAutoSave();
}
function markAsSaved() {
hasUnsavedChanges = false;
updateSaveButton();
}
function updateSaveButton() {
if (hasUnsavedChanges) {
saveButton.disabled = false;
saveButton.style.opacity = '1';
} else {
saveButton.disabled = true;
saveButton.style.opacity = '0.5';
}
}
function triggerAutoSave() {
clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(autoSave, 3000);
}
function autoSave() {
const content = editor.innerHTML;
saveContent(content);
}
function manualSave() {
if (!hasUnsavedChanges) return;
clearTimeout(autoSaveTimeout);
const content = editor.innerHTML;
saveContent(content);
}
function saveContent(content) {
statusElement.textContent = 'Saving...';
statusElement.className = '';
const formData = new FormData();
formData.append('action', 'save_dev_notes');
formData.append('notes_content', content);
formData.append('devnotes_nonce', devnotesData.nonce);
fetch(devnotesData.ajaxurl, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusElement.textContent = '';
statusElement.className = '';
markAsSaved();
} else {
statusElement.textContent = 'Save failed: ' + (data.data || 'Unknown error');
statusElement.className = 'auto-save-error';
}
})
.catch(error => {
statusElement.textContent = 'Save error: ' + error.message;
statusElement.className = 'auto-save-error';
});
}
// Event listeners for visual editor
editor.addEventListener('input', markAsChanged);
editor.addEventListener('keydown', function(e) {
// Handle tab key for indentation
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
markAsChanged();
}
});
saveButton.addEventListener('click', manualSave);
// Keyboard shortcut
document.addEventListener('keydown', function(e) {
const isCtrlS = e.ctrlKey && e.key === 's' && !e.metaKey;
const isCmdS = e.metaKey && e.key === 's' && !e.ctrlKey;
if (isCtrlS || isCmdS) {
e.preventDefault();
e.stopPropagation();
manualSave();
return false;
}
});
// Initialize
updateSaveButton();
// Set initial content if empty
if (editor.innerHTML.trim() === '') {
editor.innerHTML = '<p>Start writing your notes here...</p>';
}
});
</script>
<?php
}
// Save form submission
add_action('admin_init', function () {
if (
isset($_POST['notes_content']) &&
check_admin_referer('devnotes_save', 'devnotes_nonce') &&
current_user_can('manage_options')
) {
update_option('devnotes_data', wp_unslash($_POST['notes_content']));
add_action('admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>Notes saved successfully!</p></div>';
});
}
});