bc7d6986a8
The right-column "Leave Feedback" form on the About page has been
expanded from two radio buttons (user picked one of two) to seven
checkboxes (user can pick any), plus an optional message textarea.
The submit button — which previously called toggleSection() on
element IDs that never existed and did nothing — now AJAX-posts
to a new server-side handler that emails the site admin via
wp_mail().
FORM OPTIONS (checkboxes — multi-select)
- I have ideas to improve this plugin
- I need help with this plugin
- I found a bug
- I'd like to request a new feature
- I'd like to share my use case
- Just saying thanks
- Other
SUBMISSION FLOW
- Client-side: at least one checkbox OR a message is required; an
inline hint shows otherwise.
- AJAX POST wp_notes_submit_feedback with topics[] + message + nonce.
- Server handler (manage_options capability + nonce checked)
sanitizes input, allow-lists the seven topic keys, builds a
plain-text email body (sender + site URL + plugin version +
checked topics with pretty labels + message), and ships it to
get_option('admin_email') via wp_mail() with Reply-To set to the
submitting user.
- Inline success message replaces the form on success; inline error
lets the user retry on failure.
The toggleSection() helper that the old broken handler used is kept
defined for now — it's no longer referenced anywhere on the About
page, so it's flagged in the changelog for a future Tier-2 removal
pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1683 lines
63 KiB
PHP
1683 lines
63 KiB
PHP
<?php
|
|
/**
|
|
* A-WP-Notes - WordPress Task Management Plugin
|
|
*
|
|
* Plugin Name: A-WP-Notes
|
|
* Plugin URI: https://icanhelp.ie/wp-notes
|
|
* Description: A plugin to add your notes to the WordPress dashboard with import/export functionality
|
|
* Version: 3.1.0
|
|
* Requires at least: 5.0
|
|
* Requires PHP: 7.2
|
|
* Author: IR240474
|
|
* Author URI: https://rangersmyth.xyz/
|
|
* License: GPL v2 or later
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
* Text Domain: a-wp-notes
|
|
* Domain Path: /languages
|
|
*
|
|
* @package WP_Notes
|
|
*/
|
|
|
|
/**
|
|
* Security check and WordPress core loading
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
die;
|
|
}
|
|
|
|
// One-time initialization of plugin constants
|
|
global $wp_notes_init;
|
|
if (!isset($wp_notes_init)) {
|
|
$wp_notes_init = true;
|
|
|
|
// Plugin Constants
|
|
if (!defined('WP_NOTES_VERSION')) define('WP_NOTES_VERSION', '3.1.0');
|
|
if (!defined('WP_NOTES_FILE')) define('WP_NOTES_FILE', __FILE__);
|
|
if (!defined('WP_NOTES_PATH')) define('WP_NOTES_PATH', plugin_dir_path(__FILE__));
|
|
if (!defined('WP_NOTES_URL')) define('WP_NOTES_URL', plugin_dir_url(__FILE__));
|
|
if (!defined('WP_NOTES_BASENAME')) define('WP_NOTES_BASENAME', plugin_basename(__FILE__));
|
|
}
|
|
|
|
// Initialize error logging if not already defined
|
|
if (!function_exists('wp_notes_log_error')) {
|
|
function wp_notes_log_error($message) {
|
|
if (defined('WP_DEBUG') && WP_DEBUG === true) {
|
|
error_log('[WP Notes] ' . $message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Plugin class autoloader
|
|
spl_autoload_register(function ($class) {
|
|
$prefix = 'WP_Notes\\';
|
|
$base_dir = plugin_dir_path(__FILE__) . 'includes/';
|
|
|
|
$len = strlen($prefix);
|
|
if (strncmp($prefix, $class, $len) !== 0) {
|
|
return;
|
|
}
|
|
|
|
$relative_class = substr($class, $len);
|
|
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
|
|
|
if (file_exists($file)) {
|
|
require $file;
|
|
}
|
|
});
|
|
|
|
// Plugin initialization function
|
|
function wp_notes_init() {
|
|
// Load plugin functionality
|
|
if (class_exists('WP_Notes\\Plugin')) {
|
|
$plugin = new WP_Notes\Plugin();
|
|
$plugin->run();
|
|
}
|
|
}
|
|
|
|
// Hook into WordPress init
|
|
add_action('plugins_loaded', 'wp_notes_init', 10);
|
|
|
|
// Register activation and deactivation hooks
|
|
register_activation_hook(__FILE__, 'wp_notes_activate');
|
|
register_deactivation_hook(__FILE__, 'wp_notes_deactivate');
|
|
|
|
/**
|
|
* Plugin activation handler
|
|
*/
|
|
function wp_notes_activate() {
|
|
// Ensure PHP version is compatible
|
|
if (version_compare(PHP_VERSION, '7.2', '<')) {
|
|
deactivate_plugins(basename(__FILE__));
|
|
wp_die('This plugin requires PHP version 7.2 or higher.');
|
|
}
|
|
|
|
// Initialize plugin settings
|
|
add_option('wp_notes_version', WP_NOTES_VERSION);
|
|
|
|
// Create database tables
|
|
wp_notes_create_tables();
|
|
|
|
// Backup existing notes
|
|
$existing_notes = get_option('wp_notes', array());
|
|
$existing_done_notes = get_option('wp_done_notes', array());
|
|
|
|
if (!empty($existing_notes)) {
|
|
update_option('wp_notes_backup', $existing_notes);
|
|
}
|
|
if (!empty($existing_done_notes)) {
|
|
update_option('wp_done_notes_backup', $existing_done_notes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Plugin deactivation handler
|
|
*/
|
|
function wp_notes_deactivate() {
|
|
delete_option('wp_notes_version');
|
|
}
|
|
|
|
/**
|
|
* Create required database tables
|
|
*/
|
|
function wp_notes_create_tables() {
|
|
global $wpdb;
|
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$sql = array();
|
|
|
|
// Notes table
|
|
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wp_notes (
|
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
user_id bigint(20) NOT NULL,
|
|
note_content text NOT NULL,
|
|
note_status varchar(20) NOT NULL DEFAULT 'active',
|
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
KEY user_id (user_id),
|
|
KEY note_status (note_status)
|
|
) $charset_collate;";
|
|
|
|
foreach ($sql as $query) {
|
|
dbDelta($query);
|
|
}
|
|
}
|
|
|
|
// Initialize plugin
|
|
add_action('plugins_loaded', 'wp_notes_init');
|
|
|
|
// Set text domain for translations
|
|
function wp_notes_load_textdomain() {
|
|
load_plugin_textdomain('a-wp-notes', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
|
}
|
|
add_action('plugins_loaded', 'wp_notes_load_textdomain');
|
|
|
|
// Include required files
|
|
if (defined('WP_NOTES_PATH')) {
|
|
require_once WP_NOTES_PATH . 'inc/admin-bar.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-about.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-feedback.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-display.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-styles.php';
|
|
}
|
|
|
|
|
|
// Admin Menu
|
|
function wp_notes_admin_menu() {
|
|
add_menu_page(
|
|
'WP Notes',
|
|
'WP Notes',
|
|
'manage_options',
|
|
'wp-notes',
|
|
'wp_notes_page_callback',
|
|
'dashicons-admin-generic',
|
|
3
|
|
);
|
|
|
|
// "My Notes" — the main landing submenu. Same slug as the parent
|
|
// menu so clicking either WP Notes or My Notes lands on the same
|
|
// central dashboard (the parent's wp_notes_page_callback already
|
|
// renders form + active list + completed list).
|
|
//
|
|
// CRITICAL: callback must be empty here. WordPress registers BOTH
|
|
// the parent's and the submenu's callbacks against the same page
|
|
// hook when slugs match, and runs them in order — passing
|
|
// wp_notes_create_page as the callback caused a duplicate
|
|
// "Create a New WP Note" form to render BELOW the main page
|
|
// content. Empty callback means only the parent's renderer fires.
|
|
add_submenu_page(
|
|
'wp-notes', // Parent slug
|
|
'My Notes', // Page title (browser tab)
|
|
'My Notes', // Menu label (sidebar)
|
|
'manage_options', // Capability
|
|
'wp-notes', // Menu slug (matches parent → same page)
|
|
'' // Empty callback — see comment above
|
|
);
|
|
|
|
// Settings submenu
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'Settings',
|
|
'Settings',
|
|
'manage_options',
|
|
'wp-notes-settings',
|
|
'wp_notes_settings_page'
|
|
);
|
|
|
|
// Import/Export submenu
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'Import/Export',
|
|
'Import/Export',
|
|
'manage_options',
|
|
'wp-notes-import-export',
|
|
'wp_notes_import_export_page'
|
|
);
|
|
|
|
// About submenu — the WP Notes brand is already carried by the
|
|
// parent menu, so the submenu can be plain-spoken.
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'About', // Page title (browser tab)
|
|
'About', // Menu label (sidebar)
|
|
'manage_options',
|
|
'wp-notes-about',
|
|
'wp_notes_about_page' // Renderer in inc/wp-notes-about.php
|
|
);
|
|
|
|
// Register settings with validation callback
|
|
register_setting(
|
|
'wp_notes_settings',
|
|
'wp_notes_settings',
|
|
array(
|
|
'sanitize_callback' => 'wp_notes_validate_settings',
|
|
'default' => array(
|
|
'default_font' => 'Arial',
|
|
'default_size' => '16'
|
|
)
|
|
)
|
|
);
|
|
add_settings_section(
|
|
'wp_notes_main_section',
|
|
'General Settings',
|
|
'wp_notes_section_callback',
|
|
'wp-notes-settings'
|
|
);
|
|
add_settings_field(
|
|
'wp_notes_default_font',
|
|
'Default Font',
|
|
'wp_notes_font_callback',
|
|
'wp-notes-settings',
|
|
'wp_notes_main_section'
|
|
);
|
|
add_settings_field(
|
|
'wp_notes_default_size',
|
|
'Default Size',
|
|
'wp_notes_size_callback',
|
|
'wp-notes-settings',
|
|
'wp_notes_main_section'
|
|
);
|
|
}
|
|
|
|
// Settings validation callback
|
|
function wp_notes_validate_settings($input) {
|
|
$output = array();
|
|
|
|
// Validate font
|
|
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
|
|
$output['default_font'] = in_array($input['default_font'], $allowed_fonts) ? $input['default_font'] : 'Arial';
|
|
|
|
// Validate size (8-72px)
|
|
$size = absint($input['default_size']);
|
|
$output['default_size'] = ($size >= 8 && $size <= 72) ? $size : 16;
|
|
|
|
add_settings_error(
|
|
'wp_notes_settings',
|
|
'settings_updated',
|
|
'Settings saved successfully.',
|
|
'updated'
|
|
);
|
|
|
|
return $output;
|
|
}
|
|
|
|
// Settings page callback
|
|
function wp_notes_settings_page() {
|
|
// Show any settings messages
|
|
settings_errors('wp_notes_settings');
|
|
?>
|
|
<div class="wrap">
|
|
<h1>WP Notes Settings</h1>
|
|
<form method="post" action="options.php">
|
|
<?php
|
|
settings_fields('wp_notes_settings');
|
|
do_settings_sections('wp-notes-settings');
|
|
submit_button();
|
|
?>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// Settings section callback
|
|
function wp_notes_section_callback() {
|
|
echo '<p>Configure default settings for WP Notes.</p>';
|
|
}
|
|
|
|
// Font setting callback
|
|
function wp_notes_font_callback() {
|
|
$options = get_option('wp_notes_settings');
|
|
$current = $options['default_font'] ?? 'Arial';
|
|
?>
|
|
<select name="wp_notes_settings[default_font]">
|
|
<option value="Arial" <?php selected($current, 'Arial'); ?>>Arial</option>
|
|
<option value="Helvetica" <?php selected($current, 'Helvetica'); ?>>Helvetica</option>
|
|
<option value="Times New Roman" <?php selected($current, 'Times New Roman'); ?>>Times New Roman</option>
|
|
<option value="Verdana" <?php selected($current, 'Verdana'); ?>>Verdana</option>
|
|
</select>
|
|
<?php
|
|
}
|
|
|
|
// Size setting callback
|
|
function wp_notes_size_callback() {
|
|
$options = get_option('wp_notes_settings');
|
|
$current = $options['default_size'] ?? '16';
|
|
?>
|
|
<input type="number" name="wp_notes_settings[default_size]" value="<?php echo esc_attr($current); ?>" min="8" max="72">
|
|
<span class="description">Font size in pixels (8-72)</span>
|
|
<?php
|
|
}
|
|
|
|
// Import/Export page callback
|
|
function wp_notes_import_export_page() {
|
|
// Display settings errors
|
|
settings_errors();
|
|
|
|
if (isset($_POST['export_notes'])) {
|
|
wp_notes_export_data();
|
|
}
|
|
|
|
if (isset($_POST['import_notes']) && !empty($_FILES['import_file']['tmp_name'])) {
|
|
try {
|
|
wp_notes_import_data();
|
|
} catch (Exception $e) {
|
|
add_settings_error(
|
|
'wp_notes_import',
|
|
'import_error',
|
|
'Error importing notes: ' . esc_html($e->getMessage()),
|
|
'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get current settings for display
|
|
$options = get_option('wp_notes_settings');
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Import/Export Notes</h1>
|
|
|
|
<!-- Export Section -->
|
|
<div class="postbox">
|
|
<div class="inside">
|
|
<h2>Export Notes</h2>
|
|
<p>Download your notes as JSON or CSV file.</p>
|
|
<form method="post">
|
|
<select name="export_format">
|
|
<option value="json">JSON</option>
|
|
<option value="csv">CSV</option>
|
|
</select>
|
|
<?php wp_nonce_field('wp_notes_export', 'export_nonce'); ?>
|
|
<input type="submit" name="export_notes" class="button button-primary" value="Export Notes">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Section -->
|
|
<div class="postbox">
|
|
<div class="inside">
|
|
<h2>Import Notes</h2>
|
|
<p>Import notes from JSON or CSV file.</p>
|
|
<form method="post" enctype="multipart/form-data">
|
|
<input type="file" name="import_file" accept=".json,.csv">
|
|
<?php wp_nonce_field('wp_notes_import', 'import_nonce'); ?>
|
|
<input type="submit" name="import_notes" class="button button-primary" value="Import Notes">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// Export functionality
|
|
function wp_notes_export_data() {
|
|
if (!isset($_POST['export_nonce']) || !wp_verify_nonce($_POST['export_nonce'], 'wp_notes_export')) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
$format = $_POST['export_format'] ?? 'json';
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
$data = array(
|
|
'active_notes' => $notes,
|
|
'completed_notes' => $done_notes,
|
|
'export_date' => current_time('mysql')
|
|
);
|
|
|
|
if ($format === 'json') {
|
|
header('Content-Type: application/json');
|
|
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.json');
|
|
echo wp_json_encode($data, JSON_PRETTY_PRINT);
|
|
} else {
|
|
header('Content-Type: text/csv');
|
|
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.csv');
|
|
$output = fopen('php://output', 'w');
|
|
fputcsv($output, array('Type', 'Text', 'Created By', 'Created On', 'Status'));
|
|
|
|
foreach ($notes as $note) {
|
|
fputcsv($output, array(
|
|
'active',
|
|
$note['text'],
|
|
$note['author_name'] ?? 'Unknown',
|
|
$note['timestamp'] ?? '',
|
|
'Active'
|
|
));
|
|
}
|
|
|
|
foreach ($done_notes as $note) {
|
|
fputcsv($output, array(
|
|
'completed',
|
|
$note['text'],
|
|
$note['author_name'] ?? 'Unknown',
|
|
$note['timestamp'] ?? '',
|
|
'Completed'
|
|
));
|
|
}
|
|
fclose($output);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// Import functionality
|
|
function wp_notes_import_data() {
|
|
if (!isset($_POST['import_nonce']) || !wp_verify_nonce($_POST['import_nonce'], 'wp_notes_import')) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
$file = $_FILES['import_file'];
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
|
|
if ($ext === 'json') {
|
|
$content = file_get_contents($file['tmp_name']);
|
|
$data = json_decode($content, true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
if (isset($data['active_notes'])) {
|
|
update_option('wp_notes', $data['active_notes']);
|
|
}
|
|
if (isset($data['completed_notes'])) {
|
|
update_option('wp_done_notes', $data['completed_notes']);
|
|
}
|
|
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
|
|
} else {
|
|
add_settings_error('wp_notes_import', 'import_error', 'Invalid JSON file');
|
|
}
|
|
} elseif ($ext === 'csv') {
|
|
$handle = fopen($file['tmp_name'], 'r');
|
|
$headers = fgetcsv($handle);
|
|
$notes = array();
|
|
$done_notes = array();
|
|
|
|
while (($data = fgetcsv($handle)) !== false) {
|
|
$note = array(
|
|
'text' => $data[1],
|
|
'author_name' => $data[2],
|
|
'timestamp' => $data[3],
|
|
'color' => '#000000',
|
|
'size' => '16',
|
|
'font' => 'Arial'
|
|
);
|
|
|
|
if ($data[0] === 'active') {
|
|
$notes[] = $note;
|
|
} else {
|
|
$done_notes[] = $note;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
|
|
} else {
|
|
add_settings_error('wp_notes_import', 'import_error', 'Invalid file format. Please use JSON or CSV files.');
|
|
}
|
|
}
|
|
add_action('admin_menu', 'wp_notes_admin_menu');
|
|
|
|
// The Tools → My Notes shortcut used to live here and routed to a
|
|
// separate bare-bones form at ?page=wp-notes-create. It was removed
|
|
// as a duplicate — the My Notes page (and the admin-bar "New Note"
|
|
// shortcut that jumps to its #new-note anchor) covers the same need
|
|
// with the proper UI. Any stale bookmark to the old URL is caught
|
|
// by wp_notes_redirect_legacy_create_page() below.
|
|
|
|
// Redirect anyone hitting the legacy ?page=wp-notes-create URL to
|
|
// the My Notes page, so existing bookmarks don't 404 / show "you do
|
|
// not have sufficient permissions" after the Tools shortcut removal.
|
|
add_action('admin_init', 'wp_notes_redirect_legacy_create_page');
|
|
function wp_notes_redirect_legacy_create_page() {
|
|
if (!is_admin()) { return; }
|
|
if (!isset($_GET['page'])) { return; }
|
|
if (sanitize_key(wp_unslash($_GET['page'])) !== 'wp-notes-create') { return; }
|
|
wp_safe_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
|
|
// Enqueue Scripts
|
|
function wp_notes_enqueue_scripts() {
|
|
// wp_enqueue_script('wp-notes-activity-tracker', WP_NOTES_URL . 'js/wp-notes-activity.js', ['jquery'], null, true); // Activity tracking removed 2025-05-09
|
|
wp_enqueue_script('wp-notes-feedback', WP_NOTES_URL . 'js/wp-notes-feedback.js', ['jquery'], null, true);
|
|
wp_localize_script('wp-notes-feedback', 'wp_notes_feedback_vars', array(
|
|
'ajax_url' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('wp_notes_feedback_nonce')
|
|
));
|
|
}
|
|
add_action('admin_enqueue_scripts', 'wp_notes_enqueue_scripts');
|
|
|
|
// Emoji picker initialization
|
|
add_action('admin_footer', 'wp_notes_emoji_picker_init');
|
|
function wp_notes_emoji_picker_init() {
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
// Initialize emoji picker
|
|
function initEmojiPicker() {
|
|
// Handle emoji picker toggle
|
|
$('.emoji-input').on('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
$('.emoji-picker-dropdown').not($container.find('.emoji-picker-dropdown')).hide();
|
|
$container.find('.emoji-picker-dropdown').toggle();
|
|
});
|
|
|
|
// Handle emoji selection
|
|
$('.emoji-option').on('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var emoji = $(this).data('emoji');
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
var $noteText = $('#wp_notes_text');
|
|
|
|
// Get cursor position or selection
|
|
var start = $noteText[0].selectionStart;
|
|
var end = $noteText[0].selectionEnd;
|
|
var text = $noteText.val();
|
|
|
|
// Insert emoji at cursor position
|
|
var newText = text.substring(0, start) + emoji + text.substring(end);
|
|
$noteText.val(newText);
|
|
|
|
// Update cursor position
|
|
var newCursorPos = start + emoji.length;
|
|
$noteText[0].setSelectionRange(newCursorPos, newCursorPos);
|
|
|
|
// Focus back on textarea
|
|
$noteText.focus();
|
|
|
|
// Hide dropdown
|
|
$container.find('.emoji-picker-dropdown').hide();
|
|
});
|
|
|
|
// Close emoji picker when clicking outside
|
|
$(document).on('click', function(e) {
|
|
if (!$(e.target).closest('.emoji-picker-container').length) {
|
|
$('.emoji-picker-dropdown').hide();
|
|
}
|
|
});
|
|
|
|
// Handle keyboard navigation — focus returns to the input
|
|
// (the picker trigger) after Escape now that the dedicated
|
|
// button has been removed.
|
|
$('.emoji-picker-container').on('keydown', function(e) {
|
|
var $dropdown = $(this).find('.emoji-picker-dropdown');
|
|
var $input = $(this).find('.emoji-input');
|
|
|
|
// Toggle dropdown with Enter or Space when the input is focused
|
|
if ((e.key === 'Enter' || e.key === ' ') && e.target === $input[0]) {
|
|
e.preventDefault();
|
|
$dropdown.toggle();
|
|
}
|
|
|
|
// Close with Escape
|
|
if (e.key === 'Escape' && $dropdown.is(':visible')) {
|
|
$dropdown.hide();
|
|
$input.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
initEmojiPicker();
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// JavaScript for saving edited notes
|
|
add_action('admin_footer', 'wp_notes_add_inline_scripts');
|
|
function wp_notes_add_inline_scripts() {
|
|
echo "
|
|
<script type='text/javascript'>
|
|
jQuery(document).ready(function($) {
|
|
// Show edit form
|
|
$('.edit-note').on('click', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
$('#edit-note-' + noteId).toggle();
|
|
});
|
|
|
|
// AJAX submit the edit form
|
|
$('.edit-note-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
var formData = $(this).serialize() + '&action=wp_notes_save_edit¬e_id=' + noteId;
|
|
$.post(ajaxurl, formData, function(response) {
|
|
if (response.success) {
|
|
alert('Note updated successfully!');
|
|
location.reload(); // Reload to display updated notes
|
|
} else {
|
|
alert('Error updating note: ' + response.data);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Persist the dismissal of the empty-state notice.
|
|
// WP core's common.js attaches the X button and hides the
|
|
// notice on click; we just hook the same click and fire an
|
|
// AJAX call to set the user_meta flag so the notice does
|
|
// not reappear on the next page load.
|
|
$(document).on('click', '.wp-notes-empty .notice-dismiss', function() {
|
|
var \$notice = $(this).closest('.wp-notes-empty');
|
|
var type = \$notice.data('wp-notes-empty-type');
|
|
var nonce = \$notice.data('wp-notes-nonce');
|
|
if (!type || !nonce) { return; }
|
|
$.post(ajaxurl, {
|
|
action: 'wp_notes_dismiss_empty',
|
|
type: type,
|
|
nonce: nonce
|
|
});
|
|
});
|
|
});
|
|
</script>";
|
|
}
|
|
|
|
// Receive the "Leave Feedback" form from the About page and forward
|
|
// it to the site admin via wp_mail(). Topics is an array of short
|
|
// keys (improve / help / bug / feature / use-case / thanks / other);
|
|
// message is an optional free-text textarea. Sender info is taken
|
|
// from wp_get_current_user() so we never trust client-supplied
|
|
// identity. Capability-gated to manage_options because the About
|
|
// page is admin-only.
|
|
add_action('wp_ajax_wp_notes_submit_feedback', 'wp_notes_ajax_submit_feedback');
|
|
function wp_notes_ajax_submit_feedback() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error('Insufficient permissions.', 403);
|
|
}
|
|
check_ajax_referer('wp_notes_feedback_submit', 'nonce');
|
|
|
|
$allowed_topics = ['improve', 'help', 'bug', 'feature', 'use-case', 'thanks', 'other'];
|
|
$raw_topics = isset($_POST['topics']) && is_array($_POST['topics']) ? wp_unslash($_POST['topics']) : [];
|
|
$topics = array_values(array_intersect($allowed_topics, array_map('sanitize_key', $raw_topics)));
|
|
|
|
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
|
|
|
|
if (empty($topics) && $message === '') {
|
|
wp_send_json_error('Pick at least one topic or write a message.', 400);
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
$site = get_bloginfo('name');
|
|
$to = get_option('admin_email');
|
|
|
|
$topic_labels = [
|
|
'improve' => 'Ideas to improve the plugin',
|
|
'help' => 'Needs help with the plugin',
|
|
'bug' => 'Reporting a bug',
|
|
'feature' => 'New feature request',
|
|
'use-case' => 'Sharing a use case',
|
|
'thanks' => 'Saying thanks',
|
|
'other' => 'Other',
|
|
];
|
|
$topics_pretty = array_map(function ($t) use ($topic_labels) { return $topic_labels[$t] ?? $t; }, $topics);
|
|
|
|
$subject = sprintf('[%s] WP Notes feedback from %s', $site, $user->display_name ?: $user->user_login);
|
|
$body = "Feedback received via WP Notes → About page\n";
|
|
$body .= str_repeat('-', 48) . "\n\n";
|
|
$body .= 'From: ' . ($user->display_name ?: $user->user_login) . ' <' . $user->user_email . ">\n";
|
|
$body .= 'Site: ' . home_url() . "\n";
|
|
$body .= 'Plugin: v' . WP_NOTES_VERSION . "\n\n";
|
|
$body .= "Topics:\n";
|
|
if (!empty($topics_pretty)) {
|
|
foreach ($topics_pretty as $label) { $body .= ' - ' . $label . "\n"; }
|
|
} else {
|
|
$body .= " (none selected)\n";
|
|
}
|
|
$body .= "\nMessage:\n";
|
|
$body .= $message !== '' ? $message . "\n" : "(no message provided)\n";
|
|
|
|
$headers = [
|
|
'Content-Type: text/plain; charset=UTF-8',
|
|
'Reply-To: ' . sanitize_email($user->user_email),
|
|
];
|
|
|
|
$sent = wp_mail($to, $subject, $body, $headers);
|
|
|
|
if (!$sent) {
|
|
wp_send_json_error('Email could not be sent. Check the site mail configuration.', 500);
|
|
}
|
|
|
|
wp_send_json_success(['delivered_to' => $to]);
|
|
}
|
|
|
|
// Persist a user's dismissal of the "No active/completed notes found"
|
|
// empty-state notice so it doesn't reappear on the next page load.
|
|
// Triggered by inline JS in wp_notes_add_inline_scripts() when the
|
|
// user clicks the X on a .wp-notes-empty notice.
|
|
add_action('wp_ajax_wp_notes_dismiss_empty', 'wp_notes_ajax_dismiss_empty');
|
|
function wp_notes_ajax_dismiss_empty() {
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_send_json_error('Insufficient permissions.', 403);
|
|
}
|
|
check_ajax_referer('wp_notes_dismiss_empty', 'nonce');
|
|
|
|
$type = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : '';
|
|
if (!in_array($type, ['active', 'completed'], true)) {
|
|
wp_send_json_error('Invalid list type.', 400);
|
|
}
|
|
|
|
update_user_meta(get_current_user_id(), 'wp_notes_dismissed_empty_' . $type, 1);
|
|
wp_send_json_success(['dismissed' => $type]);
|
|
}
|
|
|
|
// Handle saving edited notes
|
|
add_action('wp_ajax_wp_notes_save_edit', 'wp_notes_save_edit');
|
|
function wp_notes_save_edit() {
|
|
// Security checks
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_send_json_error('Insufficient permissions.');
|
|
return;
|
|
}
|
|
|
|
// Security check using nonce
|
|
check_ajax_referer('wp_notes_nonce', '_wpnonce');
|
|
|
|
// Validate required fields
|
|
$required_fields = ['note_id', 'new_text', 'edit_color', 'edit_size', 'edit_font'];
|
|
foreach ($required_fields as $field) {
|
|
if (!isset($_POST[$field])) {
|
|
wp_send_json_error("Missing required field: $field");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get and sanitize data
|
|
$note_id = absint($_POST['note_id']);
|
|
$new_text = sanitize_text_field($_POST['new_text']);
|
|
$new_color = sanitize_hex_color($_POST['edit_color']);
|
|
$new_size = absint($_POST['edit_size']);
|
|
|
|
// Validate font against allowed list
|
|
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
|
|
$new_font = in_array($_POST['edit_font'], $allowed_fonts) ? $_POST['edit_font'] : 'Arial';
|
|
|
|
// Validate size range
|
|
if ($new_size < 8 || $new_size > 72) {
|
|
wp_send_json_error('Font size must be between 8 and 72 pixels.');
|
|
return;
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (!isset($notes[$note_id])) {
|
|
wp_send_json_error('Note not found.');
|
|
return;
|
|
}
|
|
|
|
// Update note with validation
|
|
$notes[$note_id] = array_merge($notes[$note_id], [
|
|
'text' => $new_text,
|
|
'color' => $new_color ?: '#000000',
|
|
'size' => $new_size,
|
|
'font' => $new_font,
|
|
'modified_timestamp' => current_time('mysql'),
|
|
'modified_by' => get_current_user_id()
|
|
]);
|
|
|
|
if (!update_option('wp_notes', $notes)) {
|
|
wp_send_json_error('Failed to update note.');
|
|
return;
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => 'Note updated successfully',
|
|
'note' => $notes[$note_id]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Main WP Notes Page - Central hub for note management
|
|
*/
|
|
function wp_notes_page_callback() {
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$total_notes = count($notes);
|
|
$total_done = count($done_notes);
|
|
|
|
// Get user settings
|
|
$settings = get_option('wp_notes_settings', array(
|
|
'default_font' => 'Arial',
|
|
'default_size' => '16'
|
|
));
|
|
?>
|
|
<div class="wrap">
|
|
<!-- Header Section with WordPress Admin Styling -->
|
|
<h1 class="wp-heading-inline">WP Notes</h1>
|
|
<span class="page-title-action">v<?php echo esc_html(WP_NOTES_VERSION); ?></span>
|
|
<hr class="wp-header-end">
|
|
|
|
<p class="description" style="max-width: 720px;">
|
|
Log your daily tasks and keep a tidy work-log inside WordPress.
|
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-about')); ?>">Read more on the About page →</a>
|
|
</p>
|
|
|
|
<!-- Note Creation Form with WordPress Admin Styling -->
|
|
<div class="postbox">
|
|
<div class="postbox-header">
|
|
<h2 class="hndle">Add New Note</h2>
|
|
</div>
|
|
<div class="inside">
|
|
<form method="post" class="wp-notes-form">
|
|
<div class="form-field">
|
|
<label for="wp_notes_text" class="screen-reader-text">Note Content:</label>
|
|
<textarea
|
|
name="wp_notes_text"
|
|
id="wp_notes_text"
|
|
class="large-text"
|
|
required
|
|
rows="4"
|
|
aria-label="Enter your note content"
|
|
placeholder="Enter your note here..."></textarea>
|
|
</div>
|
|
|
|
<div class="wp-notes-formatting form-field">
|
|
<fieldset>
|
|
<legend class="screen-reader-text">Note Formatting Options</legend>
|
|
|
|
<div class="formatting-options">
|
|
<div class="format-option">
|
|
<label for="wp_notes_color" class="format-label">
|
|
<span class="dashicons dashicons-color-picker"></span>
|
|
Text Color
|
|
</label>
|
|
<input
|
|
type="color"
|
|
name="wp_notes_color"
|
|
id="wp_notes_color"
|
|
value="#000000"
|
|
aria-label="Choose text color">
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_size" class="format-label">
|
|
<span class="dashicons dashicons-editor-textcolor"></span>
|
|
Size (px)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="wp_notes_size"
|
|
id="wp_notes_size"
|
|
min="8"
|
|
max="72"
|
|
value="16"
|
|
class="small-text"
|
|
aria-label="Choose text size">
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_font" class="format-label">
|
|
<span class="dashicons dashicons-editor-paragraph"></span>
|
|
Font
|
|
</label>
|
|
<select
|
|
name="wp_notes_font"
|
|
id="wp_notes_font"
|
|
class="regular-text"
|
|
aria-label="Choose font">
|
|
<option value="Arial">Arial</option>
|
|
<option value="Helvetica">Helvetica</option>
|
|
<option value="Times New Roman">Times New Roman</option>
|
|
<option value="Verdana">Verdana</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_emoji" class="format-label">
|
|
<span class="dashicons dashicons-smiley"></span>
|
|
Emoji
|
|
</label>
|
|
<div class="emoji-picker-container">
|
|
<input
|
|
type="text"
|
|
name="wp_notes_emoji"
|
|
id="wp_notes_emoji"
|
|
placeholder="Click to add emoji"
|
|
class="small-text emoji-input"
|
|
aria-label="Add emoji"
|
|
readonly>
|
|
<div class="emoji-picker-dropdown" style="display: none;">
|
|
<div class="emoji-list">
|
|
<!-- Common emojis -->
|
|
<button type="button" class="emoji-option" data-emoji="😊">😊</button>
|
|
<button type="button" class="emoji-option" data-emoji="👍">👍</button>
|
|
<button type="button" class="emoji-option" data-emoji="✅">✅</button>
|
|
<button type="button" class="emoji-option" data-emoji="⭐">⭐</button>
|
|
<button type="button" class="emoji-option" data-emoji="📝">📝</button>
|
|
<button type="button" class="emoji-option" data-emoji="❗">❗</button>
|
|
<button type="button" class="emoji-option" data-emoji="❓">❓</button>
|
|
<button type="button" class="emoji-option" data-emoji="⚠️">⚠️</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
|
|
<div class="submit-button">
|
|
<input
|
|
type="submit"
|
|
value="Add Note"
|
|
class="button button-primary button-large"
|
|
aria-label="Add new note">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.wp-notes-form .form-field { margin-bottom: 1em; }
|
|
.wp-notes-formatting { background: #f9f9f9; padding: 15px; border-radius: 4px; }
|
|
.formatting-options { display: flex; gap: 20px; flex-wrap: wrap; align-items: center; }
|
|
.format-option { display: flex; flex-direction: column; gap: 5px; }
|
|
.format-label { display: flex; align-items: center; gap: 5px; }
|
|
.submit-button { margin-top: 1em; }
|
|
.emoji-picker-container { position: relative; }
|
|
.emoji-picker-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid #ccd0d4;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
z-index: 100;
|
|
}
|
|
.emoji-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 5px;
|
|
}
|
|
.emoji-option {
|
|
border: none;
|
|
background: none;
|
|
font-size: 20px;
|
|
padding: 5px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
.emoji-option:hover {
|
|
background: #f0f0f1;
|
|
}
|
|
.emoji-input {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<!-- Notes List -->
|
|
<h2>Notes Todo List:</h2>
|
|
<!-- Active Notes List -->
|
|
<div class="wp-notes-active" id="active-notes">
|
|
<?php wp_notes_display_notes('active'); ?>
|
|
</div>
|
|
|
|
<!-- Completed Notes List -->
|
|
<div class="wp-notes-completed" id="completed-notes">
|
|
<?php wp_notes_display_notes('completed'); ?>
|
|
</div>
|
|
|
|
<!-- Footer: support link (lives at the BOTTOM, not the top) -->
|
|
<p class="wp-notes-footer-support">
|
|
<a href="https://www.buymeacoffee.com/davidkeanek" target="_blank" rel="noopener"
|
|
class="button button-secondary"
|
|
aria-label="Support the developer by buying them a coffee">
|
|
☕ Buy me a coffee
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// List Table Function
|
|
function wp_notes_list_table() {
|
|
// Check user capabilities
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (!$notes) {
|
|
echo '<p>' . esc_html__('No notes found.', 'a-wp-notes') . '</p>';
|
|
return;
|
|
}
|
|
|
|
/* Dashboard widget table. No checkbox column — bulk actions are not
|
|
wired up; re-add when there is something to act on. */
|
|
echo '<table class="wp-list-table widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" class="manage-column">Note</th>
|
|
<th scope="col" class="manage-column">Created By</th>
|
|
<th scope="col" class="manage-column">Created On</th>
|
|
<th scope="col" class="manage-column">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>';
|
|
|
|
foreach ($notes as $key => $note) {
|
|
$text = esc_html($note['text']);
|
|
$color = esc_attr($note['color'] ?? '#000000');
|
|
$size = esc_attr($note['size'] ?? '14');
|
|
$font = esc_attr($note['font'] ?? 'Arial');
|
|
$timestamp = esc_html($note['timestamp'] ?? current_time('mysql'));
|
|
$author = esc_html($note['author_name'] ?? 'Unknown');
|
|
|
|
echo "<tr>
|
|
<td style='color: $color; font-size: {$size}px; font-family: $font;'>$text</td>
|
|
<td>$author</td>
|
|
<td>$timestamp</td>
|
|
<td>
|
|
<form method='post' style='display:inline;'>
|
|
<input type='hidden' name='note_id' value='$key'>
|
|
<button type='submit' name='mark_done' value='Mark as Done' class='button button-secondary'>Mark as Done</button>
|
|
</form>
|
|
<button type='button' class='button edit-note' data-note-id='$key'>Edit</button>
|
|
<div id='edit-note-$key' style='display:none;'>
|
|
<form class='edit-note-form' data-note-id='$key'>
|
|
" . wp_nonce_field('wp_notes_nonce', '_wpnonce', true, false) . "
|
|
<input type='hidden' name='note_id' value='$key'>
|
|
<textarea name='new_text'>$text</textarea><br>
|
|
<input type='color' name='edit_color' value='$color'><br>
|
|
<input type='number' name='edit_size' value='$size' min='8' max='72'><br>
|
|
<select name='edit_font'>
|
|
<option value='Arial' " . selected('Arial', $font, false) . ">Arial</option>
|
|
<option value='Helvetica' " . selected('Helvetica', $font, false) . ">Helvetica</option>
|
|
<option value='Times New Roman' " . selected('Times New Roman', $font, false) . ">Times New Roman</option>
|
|
<option value='Verdana' " . selected('Verdana', $font, false) . ">Verdana</option>
|
|
</select><br>
|
|
<button type='submit' class='button button-primary'>Save</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>";
|
|
}
|
|
|
|
echo '</tbody></table>';
|
|
|
|
// JavaScript for form toggling and AJAX submissions
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
// Show edit form
|
|
$('.edit-note').on('click', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
$('.edit-note-form').not('#edit-note-' + noteId).hide();
|
|
$('#edit-note-' + noteId).toggle();
|
|
});
|
|
|
|
// AJAX submit the edit form
|
|
$('.edit-note-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
var $form = $(this);
|
|
var $submitButton = $form.find('button[type="submit"]');
|
|
var noteId = $form.data('note-id');
|
|
|
|
// Log form data for debugging
|
|
console.log('Form data:', $form.serialize());
|
|
console.log('Note ID:', noteId);
|
|
|
|
// Disable submit button and show loading state
|
|
$submitButton.prop('disabled', true).text('Saving...');
|
|
|
|
// AJAX request
|
|
$.ajax({
|
|
url: ajaxurl,
|
|
type: 'POST',
|
|
data: $form.serialize() + '&action=wp_notes_save_edit¬e_id=' + noteId,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + (response.data || 'Unknown error occurred'));
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
alert('Server error: ' + error);
|
|
},
|
|
complete: function() {
|
|
$submitButton.prop('disabled', false).text('Save');
|
|
}
|
|
});
|
|
|
|
// Emoji Picker Functionality
|
|
$('.emoji-picker-button, .emoji-input').on('click', function(e) {
|
|
e.preventDefault();
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
$container.find('.emoji-picker-dropdown').toggle();
|
|
});
|
|
|
|
// Handle emoji selection
|
|
$('.emoji-option').on('click', function() {
|
|
var emoji = $(this).data('emoji');
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
var $input = $container.find('.emoji-input');
|
|
|
|
// Append emoji to note text
|
|
var $noteText = $('#wp_notes_text');
|
|
var currentPosition = $noteText[0].selectionStart;
|
|
var text = $noteText.val();
|
|
var newText = text.slice(0, currentPosition) + emoji + text.slice(currentPosition);
|
|
$noteText.val(newText);
|
|
|
|
// Update cursor position
|
|
$noteText[0].setSelectionRange(currentPosition + emoji.length, currentPosition + emoji.length);
|
|
$noteText.focus();
|
|
|
|
// Hide dropdown
|
|
$container.find('.emoji-picker-dropdown').hide();
|
|
});
|
|
|
|
// Close emoji picker when clicking outside
|
|
$(document).on('click', function(e) {
|
|
if (!$(e.target).closest('.emoji-picker-container').length) {
|
|
$('.emoji-picker-dropdown').hide();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// Done Notes Table Function
|
|
function wp_notes_done_table() {
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
if (!$done_notes) {
|
|
echo '<p>' . esc_html__('No completed notes.', 'a-wp-notes') . '</p>';
|
|
return;
|
|
}
|
|
|
|
echo '<form method="post"><table class="wp-list-table widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" class="manage-column column-cb check-column">
|
|
<input type="checkbox" id="select-all-done">
|
|
</th>
|
|
<th scope="col">Note</th>
|
|
<th scope="col">Created By</th>
|
|
<th scope="col">Created On</th>
|
|
<th scope="col">Completed By</th>
|
|
<th scope="col">Completed On</th>
|
|
<th scope="col">Last Modified</th>
|
|
<th scope="col">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>';
|
|
|
|
foreach ($done_notes as $key => $note) {
|
|
echo '<tr>
|
|
<td><input type="checkbox" name="done_ids[]" value="' . esc_attr($key) . '" class="done-checkbox"></td>
|
|
<td style="color: ' . esc_attr($note['color']) . '; font-size: 14px; font-family: ' . esc_attr($note['font']) . ';">'
|
|
. esc_html($note['text']) . '</td>
|
|
<td>' . esc_html($note['author_name'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['timestamp'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['completed_by'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['completed_on'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['last_modified'] ?? 'N/A') . '</td>
|
|
<td>
|
|
<input type="submit" name="restore_note" value="Restore" class="button button-secondary">
|
|
</td>
|
|
</tr>';
|
|
}
|
|
|
|
echo '</tbody></table></form>';
|
|
|
|
// Add JavaScript for select all functionality
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
$('#select-all-done').change(function() {
|
|
$('.done-checkbox').prop('checked', $(this).prop('checked'));
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// Handle note actions
|
|
add_action('admin_init', 'wp_notes_handle_actions');
|
|
function wp_notes_handle_actions() {
|
|
$current_user = wp_get_current_user();
|
|
|
|
// Check if we're adding a new note
|
|
if (isset($_POST['wp_notes_text'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
// Create the new note
|
|
$new_note = array(
|
|
'text' => sanitize_text_field($_POST['wp_notes_text']),
|
|
'color' => sanitize_hex_color($_POST['wp_notes_color']) ?: '#000000',
|
|
'size' => absint($_POST['wp_notes_size']) ?: 16,
|
|
'font' => sanitize_text_field($_POST['wp_notes_font']) ?: 'Arial',
|
|
'timestamp' => current_time('mysql'),
|
|
'author' => $current_user->ID,
|
|
'author_name' => $current_user->display_name
|
|
);
|
|
|
|
$notes[] = $new_note;
|
|
update_option('wp_notes', $notes);
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
// Handle "Mark as Done" submissions
|
|
if (isset($_POST['mark_done']) && isset($_POST['note_id'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$note_id = absint($_POST['note_id']);
|
|
|
|
if (isset($notes[$note_id])) {
|
|
$note = $notes[$note_id];
|
|
$note['completed_by'] = $current_user->display_name;
|
|
$note['completed_on'] = current_time('mysql');
|
|
$done_notes[] = $note;
|
|
unset($notes[$note_id]);
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Handle marking notes as done if 'mark_done' action is triggered
|
|
if (isset($_POST['mark_done']) && isset($_POST['note_ids'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
// Move selected notes to done list
|
|
foreach ($notes as $key => $note) {
|
|
if (in_array($key, $_POST['note_ids'])) {
|
|
$note['completed_by'] = $current_user->display_name;
|
|
$note['completed_on'] = current_time('mysql');
|
|
$done_notes[] = $note;
|
|
unset($notes[$key]); // Remove from active notes
|
|
}
|
|
}
|
|
|
|
// Update both lists in the database
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
|
|
// Redirect to avoid form resubmission
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
// Handle restore note action
|
|
if (isset($_POST['restore_note']) && isset($_POST['done_ids'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$current_user = wp_get_current_user();
|
|
$new_done_notes = array();
|
|
|
|
foreach ($done_notes as $key => $note) {
|
|
if (in_array($key, $_POST['done_ids'])) {
|
|
$note['last_modified'] = current_time('mysql');
|
|
$note['restored_by'] = $current_user->display_name;
|
|
$notes[] = $note;
|
|
} else {
|
|
$new_done_notes[] = $note;
|
|
}
|
|
}
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $new_done_notes);
|
|
}
|
|
|
|
// Handle edit note action
|
|
if (isset($_POST['edit_note']) && isset($_POST['note_ids'])) {
|
|
// Add edit functionality here
|
|
$notes = get_option('wp_notes', array());
|
|
$current_user = wp_get_current_user();
|
|
|
|
foreach ($_POST['note_ids'] as $key) {
|
|
if (isset($notes[$key])) {
|
|
$notes[$key]['last_modified'] = current_time('mysql');
|
|
$notes[$key]['modified_by'] = $current_user->display_name;
|
|
}
|
|
}
|
|
|
|
update_option('wp_notes', $notes);
|
|
}
|
|
}
|
|
|
|
// AJAX handler for editing a note
|
|
add_action('wp_ajax_wp_notes_edit_note', 'wp_notes_edit_note');
|
|
function wp_notes_edit_note() {
|
|
if (!isset($_POST['note_id']) || !isset($_POST['edit_text'])) {
|
|
wp_send_json_error('Invalid request');
|
|
return;
|
|
}
|
|
|
|
$note_id = absint($_POST['note_id']);
|
|
$new_text = sanitize_text_field($_POST['edit_text']);
|
|
$new_color = sanitize_hex_color($_POST['edit_color']);
|
|
$new_size = absint($_POST['edit_size']);
|
|
$new_font = sanitize_text_field($_POST['edit_font']);
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (isset($notes[$note_id])) {
|
|
$notes[$note_id]['text'] = $new_text;
|
|
$notes[$note_id]['color'] = $new_color;
|
|
$notes[$note_id]['size'] = $new_size;
|
|
$notes[$note_id]['font'] = $new_font;
|
|
|
|
update_option('wp_notes', $notes);
|
|
wp_send_json_success();
|
|
} else {
|
|
wp_send_json_error('Note not found.');
|
|
}
|
|
}
|
|
|
|
// Optimize the restore function for performance
|
|
function wp_notes_restore($note_id) {
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (isset($done_notes[$note_id])) {
|
|
$notes[$note_id] = $done_notes[$note_id];
|
|
unset($done_notes[$note_id]);
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
}
|
|
}
|
|
|
|
// Dashboard widget
|
|
function wp_notes_dashboard_widget() {
|
|
wp_notes_list_table();
|
|
}
|
|
|
|
function wp_notes_add_dashboard_widgets() {
|
|
wp_add_dashboard_widget(
|
|
'wp_notes_dashboard_widget',
|
|
'WP Notes',
|
|
'wp_notes_dashboard_widget'
|
|
);
|
|
}
|
|
add_action('wp_dashboard_setup', 'wp_notes_add_dashboard_widgets');
|
|
|
|
// Register Custom Post Type for notes
|
|
function wp_notes_register_cpt() {
|
|
$labels = array(
|
|
'name' => __('Notes', 'a-wp-notes'),
|
|
'singular_name' => __('Note', 'a-wp-notes'),
|
|
'menu_name' => __('WP Notes', 'a-wp-notes'),
|
|
'add_new' => __('Add New', 'a-wp-notes'),
|
|
'add_new_item' => __('Add New Note', 'a-wp-notes'),
|
|
'edit_item' => __('Edit Note', 'a-wp-notes'),
|
|
'new_item' => __('New Note', 'a-wp-notes'),
|
|
'view_item' => __('View Note', 'a-wp-notes'),
|
|
'search_items' => __('Search Notes', 'a-wp-notes'),
|
|
'not_found' => __('No notes found', 'a-wp-notes'),
|
|
'not_found_in_trash'=> __('No notes found in trash', 'a-wp-notes'),
|
|
);
|
|
|
|
$args = array(
|
|
'labels' => $labels,
|
|
'public' => false,
|
|
// show_ui=false: the live UI stores notes in wp_options, not as
|
|
// CPT posts. show_ui=true caused WordPress to auto-inject "All
|
|
// Notes" and "Add New" submenus that pointed at post-new.php and
|
|
// routed users into the standard post editor — which writes to
|
|
// the wrong storage. The CPT remains registered so the
|
|
// wp_notes_migrate_to_cpt() helper can still use wp_insert_post.
|
|
'show_ui' => false,
|
|
'show_in_menu' => false,
|
|
'capability_type' => 'post',
|
|
'hierarchical' => false,
|
|
'supports' => array('title', 'editor', 'author', 'custom-fields'),
|
|
'has_archive' => false,
|
|
'menu_position' => null,
|
|
'show_in_rest' => false,
|
|
);
|
|
|
|
register_post_type('wp_note', $args);
|
|
|
|
// Register taxonomy for note categories
|
|
$tax_labels = array(
|
|
'name' => __('Note Categories', 'a-wp-notes'),
|
|
'singular_name' => __('Note Category', 'a-wp-notes'),
|
|
'search_items' => __('Search Categories', 'a-wp-notes'),
|
|
'all_items' => __('All Categories', 'a-wp-notes'),
|
|
'parent_item' => __('Parent Category', 'a-wp-notes'),
|
|
'parent_item_colon' => __('Parent Category:', 'a-wp-notes'),
|
|
'edit_item' => __('Edit Category', 'a-wp-notes'),
|
|
'update_item' => __('Update Category', 'a-wp-notes'),
|
|
'add_new_item' => __('Add New Category', 'a-wp-notes'),
|
|
'new_item_name' => __('New Category Name', 'a-wp-notes'),
|
|
'menu_name' => __('Categories', 'a-wp-notes'),
|
|
);
|
|
|
|
register_taxonomy('wp_note_category', 'wp_note', array(
|
|
'hierarchical' => true,
|
|
'labels' => $tax_labels,
|
|
// Hidden alongside the parent CPT — live UI doesn't surface
|
|
// categories yet, and showing the taxonomy in admin with the
|
|
// parent hidden would just dead-link.
|
|
'show_ui' => false,
|
|
'show_admin_column' => false,
|
|
'query_var' => true,
|
|
'show_in_rest' => false,
|
|
));
|
|
}
|
|
add_action('init', 'wp_notes_register_cpt');
|
|
|
|
// Add custom meta box for note properties
|
|
function wp_notes_add_meta_boxes() {
|
|
add_meta_box(
|
|
'wp_notes_properties',
|
|
__('Note Properties', 'a-wp-notes'),
|
|
'wp_notes_render_properties_meta_box',
|
|
'wp_note',
|
|
'side',
|
|
'high'
|
|
);
|
|
}
|
|
add_action('add_meta_boxes', 'wp_notes_add_meta_boxes');
|
|
|
|
// Render meta box content
|
|
function wp_notes_render_properties_meta_box($post) {
|
|
// Add nonce for security
|
|
wp_nonce_field('wp_notes_meta_box', 'wp_notes_meta_box_nonce');
|
|
|
|
// Get current values
|
|
$color = get_post_meta($post->ID, '_wp_note_color', true) ?: '#000000';
|
|
$size = get_post_meta($post->ID, '_wp_note_size', true) ?: '16';
|
|
$font = get_post_meta($post->ID, '_wp_note_font', true) ?: 'Arial';
|
|
$status = get_post_meta($post->ID, '_wp_note_status', true) ?: 'active';
|
|
?>
|
|
<p>
|
|
<label for="wp_note_color"><?php _e('Color:', 'a-wp-notes'); ?></label><br>
|
|
<input type="color" id="wp_note_color" name="wp_note_color" value="<?php echo esc_attr($color); ?>">
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_size"><?php _e('Size:', 'a-wp-notes'); ?></label><br>
|
|
<input type="number" id="wp_note_size" name="wp_note_size" value="<?php echo esc_attr($size); ?>" min="8" max="72">
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_font"><?php _e('Font:', 'a-wp-notes'); ?></label><br>
|
|
<select id="wp_note_font" name="wp_note_font">
|
|
<option value="Arial" <?php selected($font, 'Arial'); ?>>Arial</option>
|
|
<option value="Helvetica" <?php selected($font, 'Helvetica'); ?>>Helvetica</option>
|
|
<option value="Times New Roman" <?php selected($font, 'Times New Roman'); ?>>Times New Roman</option>
|
|
<option value="Verdana" <?php selected($font, 'Verdana'); ?>>Verdana</option>
|
|
</select>
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_status"><?php _e('Status:', 'a-wp-notes'); ?></label><br>
|
|
<select id="wp_note_status" name="wp_note_status">
|
|
<option value="active" <?php selected($status, 'active'); ?>><?php _e('Active', 'a-wp-notes'); ?></option>
|
|
<option value="completed" <?php selected($status, 'completed'); ?>><?php _e('Completed', 'a-wp-notes'); ?></option>
|
|
</select>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
// Save meta box data
|
|
function wp_notes_save_meta_box_data($post_id) {
|
|
// Check if our nonce is set and verify it
|
|
if (!isset($_POST['wp_notes_meta_box_nonce']) ||
|
|
!wp_verify_nonce($_POST['wp_notes_meta_box_nonce'], 'wp_notes_meta_box')) {
|
|
return;
|
|
}
|
|
|
|
// Don't save during autosave
|
|
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
|
return;
|
|
}
|
|
|
|
// Check user permissions
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return;
|
|
}
|
|
|
|
// Save the meta fields
|
|
$fields = array(
|
|
'_wp_note_color' => 'sanitize_hex_color',
|
|
'_wp_note_size' => 'absint',
|
|
'_wp_note_font' => 'sanitize_text_field',
|
|
'_wp_note_status' => 'sanitize_text_field'
|
|
);
|
|
|
|
foreach ($fields as $key => $sanitize_callback) {
|
|
if (isset($_POST[str_replace('_', '', $key)])) {
|
|
$value = call_user_func($sanitize_callback, $_POST[str_replace('_', '', $key)]);
|
|
update_post_meta($post_id, $key, $value);
|
|
}
|
|
}
|
|
}
|
|
add_action('save_post_wp_note', 'wp_notes_save_meta_box_data');
|
|
|
|
// Data Migration Tool
|
|
function wp_notes_migrate_to_cpt() {
|
|
$existing_notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$migrated_count = 0;
|
|
$errors = array();
|
|
|
|
// Start migration
|
|
foreach ($existing_notes as $note) {
|
|
$post_data = array(
|
|
'post_title' => wp_trim_words($note['text'], 10, '...'),
|
|
'post_content' => $note['text'],
|
|
'post_status' => 'publish',
|
|
'post_type' => 'wp_note',
|
|
'post_author' => $note['author'] ?? get_current_user_id(),
|
|
);
|
|
|
|
// Insert the post
|
|
$post_id = wp_insert_post($post_data, true);
|
|
|
|
if (!is_wp_error($post_id)) {
|
|
// Add note meta data
|
|
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
|
|
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
|
|
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
|
|
update_post_meta($post_id, '_wp_note_status', 'active');
|
|
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
|
|
$migrated_count++;
|
|
} else {
|
|
$errors[] = $post_id->get_error_message();
|
|
}
|
|
}
|
|
|
|
// Migrate completed notes
|
|
foreach ($done_notes as $note) {
|
|
$post_data = array(
|
|
'post_title' => wp_trim_words($note['text'], 10, '...'),
|
|
'post_content' => $note['text'],
|
|
'post_status' => 'publish',
|
|
'post_type' => 'wp_note',
|
|
'post_author' => $note['author'] ?? get_current_user_id(),
|
|
);
|
|
|
|
$post_id = wp_insert_post($post_data, true);
|
|
|
|
if (!is_wp_error($post_id)) {
|
|
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
|
|
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
|
|
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
|
|
update_post_meta($post_id, '_wp_note_status', 'completed');
|
|
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_completed_by', $note['completed_by'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_completed_on', $note['completed_on'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
|
|
$migrated_count++;
|
|
} else {
|
|
$errors[] = $post_id->get_error_message();
|
|
}
|
|
}
|
|
|
|
// Create backup of old data
|
|
update_option('wp_notes_pre_migration_backup', array(
|
|
'notes' => $existing_notes,
|
|
'done_notes' => $done_notes,
|
|
'migration_date' => current_time('mysql')
|
|
));
|
|
|
|
return array(
|
|
'success' => true,
|
|
'migrated_count' => $migrated_count,
|
|
'errors' => $errors
|
|
);
|
|
}
|
|
|
|
// Add migration notice for admin
|
|
function wp_notes_migration_notice() {
|
|
if (!current_user_can('manage_options')) {
|
|
return;
|
|
}
|
|
|
|
$migrated = get_option('wp_notes_migration_completed');
|
|
if (!$migrated) {
|
|
?>
|
|
<div class="notice notice-info is-dismissible">
|
|
<p>
|
|
<?php _e('WP Notes needs to migrate your existing notes to the new storage system.', 'a-wp-notes'); ?>
|
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-settings&action=migrate')); ?>" class="button button-primary">
|
|
<?php _e('Start Migration', 'a-wp-notes'); ?>
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
}
|
|
add_action('admin_notices', 'wp_notes_migration_notice');
|
|
|
|
// Handle migration request
|
|
function wp_notes_handle_migration() {
|
|
if (isset($_GET['page']) && $_GET['page'] === 'wp-notes-settings' &&
|
|
isset($_GET['action']) && $_GET['action'] === 'migrate') {
|
|
|
|
check_admin_referer('wp_notes_migration');
|
|
|
|
$result = wp_notes_migrate_to_cpt();
|
|
|
|
if ($result['success']) {
|
|
update_option('wp_notes_migration_completed', true);
|
|
add_settings_error(
|
|
'wp_notes_migration',
|
|
'migration_success',
|
|
sprintf(
|
|
__('Successfully migrated %d notes. %s', 'a-wp-notes'),
|
|
$result['migrated_count'],
|
|
!empty($result['errors']) ? 'Errors: ' . implode(', ', $result['errors']) : ''
|
|
),
|
|
'updated'
|
|
);
|
|
} else {
|
|
add_settings_error(
|
|
'wp_notes_migration',
|
|
'migration_error',
|
|
__('Migration failed. Please try again or contact support.', 'a-wp-notes'),
|
|
'error'
|
|
);
|
|
}
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes-settings'));
|
|
exit;
|
|
}
|
|
}
|
|
add_action('admin_init', 'wp_notes_handle_migration');
|
|
|
|
// End of wp-notes.php
|
|
?>
|