Dev notes

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">
                    &lt;/&gt; 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 = '&lt;/&gt; 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>';
        });
    }
});
Click to Copy