From bfb24f078d85a9ff0f18a856a873d32d2bc73da1 Mon Sep 17 00:00:00 2001 From: magdev Date: Wed, 21 Jan 2026 22:52:51 +0100 Subject: [PATCH] Add live search to admin licenses overview - Add AJAX handler for real-time license search - Create admin-licenses.js with debounced search and keyboard navigation - Display search results with highlighted matches - Support navigation with arrow keys and Enter to select - Add CSS for dropdown results styling Co-Authored-By: Claude Opus 4.5 --- assets/css/admin.css | 111 ++++++++++++++++ assets/js/admin-licenses.js | 237 ++++++++++++++++++++++++++++++++++ src/Admin/AdminController.php | 64 +++++++++ 3 files changed, 412 insertions(+) create mode 100644 assets/js/admin-licenses.js diff --git a/assets/css/admin.css b/assets/css/admin.css index cca0628..3cda32c 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -468,3 +468,114 @@ .licenses-table .license-actions { width: 220px; } + +/* Live Search Styles */ +.wclp-live-search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + min-width: 400px; + max-width: 600px; + max-height: 400px; + overflow-y: auto; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 0 0 4px 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; +} + +.wclp-live-search-results:empty { + display: none; +} + +.wclp-search-result-item { + padding: 12px 15px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.wclp-search-result-item:last-child { + border-bottom: none; +} + +.wclp-search-result-item:hover, +.wclp-search-result-item.active { + background-color: #f0f6fc; +} + +.wclp-result-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 4px; +} + +.wclp-result-key code { + font-size: 13px; + background: #f0f0f0; + padding: 2px 6px; + border-radius: 3px; +} + +.wclp-result-key mark { + background-color: #fff8c5; + padding: 0; +} + +.wclp-result-details { + display: flex; + gap: 15px; + font-size: 12px; + color: #646970; + margin-bottom: 2px; +} + +.wclp-result-domain mark { + background-color: #fff8c5; + padding: 0; +} + +.wclp-result-product { + color: #2271b1; +} + +.wclp-result-customer { + font-size: 11px; + color: #8c8f94; +} + +.wclp-result-customer small { + opacity: 0.8; +} + +.wclp-search-loading, +.wclp-search-no-results, +.wclp-search-error { + padding: 15px; + text-align: center; + color: #646970; + font-size: 13px; +} + +.wclp-search-loading .spinner { + float: none; + margin: 0 5px 0 0; + vertical-align: middle; +} + +.wclp-search-error { + color: #d63638; +} + +/* Search box positioning */ +.wclp-filter-form .search-box { + position: relative; +} + +#license-search-input { + width: 280px; +} diff --git a/assets/js/admin-licenses.js b/assets/js/admin-licenses.js new file mode 100644 index 0000000..3b866e8 --- /dev/null +++ b/assets/js/admin-licenses.js @@ -0,0 +1,237 @@ +/** + * WC Licensed Product - Admin Licenses Live Search + * + * @package Jeremias\WcLicensedProduct + */ + +(function($) { + 'use strict'; + + var searchTimeout = null; + var $searchInput = $('#license-search-input'); + var $resultsDropdown = null; + var isSearching = false; + + /** + * Initialize live search + */ + function init() { + if (!$searchInput.length) { + return; + } + + // Create results dropdown + $resultsDropdown = $('
'); + $searchInput.parent().css('position', 'relative').append($resultsDropdown); + + // Bind events + $searchInput.on('input', handleSearchInput); + $searchInput.on('keydown', handleKeydown); + $searchInput.on('focus', function() { + if ($resultsDropdown.children().length > 0) { + $resultsDropdown.show(); + } + }); + + // Close dropdown when clicking outside + $(document).on('click', function(e) { + if (!$(e.target).closest('.search-box').length) { + $resultsDropdown.hide(); + } + }); + } + + /** + * Handle search input with debouncing + */ + function handleSearchInput() { + var query = $searchInput.val().trim(); + + // Clear previous timeout + if (searchTimeout) { + clearTimeout(searchTimeout); + } + + // Hide results if query is too short + if (query.length < 2) { + $resultsDropdown.hide().empty(); + return; + } + + // Show loading state + showLoading(); + + // Debounce search + searchTimeout = setTimeout(function() { + performSearch(query); + }, 300); + } + + /** + * Handle keyboard navigation + */ + function handleKeydown(e) { + var $items = $resultsDropdown.find('.wclp-search-result-item'); + var $active = $items.filter('.active'); + var index = $items.index($active); + + switch (e.keyCode) { + case 40: // Down arrow + e.preventDefault(); + if (index < $items.length - 1) { + $items.removeClass('active'); + $items.eq(index + 1).addClass('active'); + } else if (index === -1 && $items.length > 0) { + $items.eq(0).addClass('active'); + } + break; + + case 38: // Up arrow + e.preventDefault(); + if (index > 0) { + $items.removeClass('active'); + $items.eq(index - 1).addClass('active'); + } + break; + + case 13: // Enter + if ($active.length) { + e.preventDefault(); + window.location.href = $active.data('url'); + } + break; + + case 27: // Escape + $resultsDropdown.hide(); + break; + } + } + + /** + * Show loading state + */ + function showLoading() { + $resultsDropdown.html( + '
' + + ' ' + + wclpAdmin.strings.searching + + '
' + ).show(); + } + + /** + * Perform AJAX search + */ + function performSearch(query) { + if (isSearching) { + return; + } + + isSearching = true; + + $.ajax({ + url: wclpAdmin.ajaxUrl, + type: 'GET', + data: { + action: 'wclp_live_search', + nonce: wclpAdmin.nonce, + search: query + }, + success: function(response) { + if (response.success && response.data.results) { + renderResults(response.data.results, query); + } else { + showNoResults(); + } + }, + error: function() { + $resultsDropdown.html( + '
' + wclpAdmin.strings.error + '
' + ).show(); + }, + complete: function() { + isSearching = false; + } + }); + } + + /** + * Render search results + */ + function renderResults(results, query) { + if (results.length === 0) { + showNoResults(); + return; + } + + var html = ''; + results.forEach(function(item) { + var statusClass = 'license-status-' + item.status; + var highlightedKey = highlightMatch(item.license_key, query); + var highlightedDomain = highlightMatch(item.domain, query); + + html += '
' + + '
' + + '' + highlightedKey + '' + + '' + escapeHtml(item.status) + '' + + '
' + + '
' + + '' + highlightedDomain + '' + + '' + escapeHtml(item.product_name) + '' + + '
' + + '
' + + escapeHtml(item.customer_name) + + (item.customer_email ? ' (' + escapeHtml(item.customer_email) + ')' : '') + + '
' + + '
'; + }); + + $resultsDropdown.html(html).show(); + + // Make items clickable + $resultsDropdown.find('.wclp-search-result-item').on('click', function() { + window.location.href = $(this).data('url'); + }).on('mouseenter', function() { + $resultsDropdown.find('.wclp-search-result-item').removeClass('active'); + $(this).addClass('active'); + }); + } + + /** + * Show no results message + */ + function showNoResults() { + $resultsDropdown.html( + '
' + wclpAdmin.strings.noResults + '
' + ).show(); + } + + /** + * Highlight matching text + */ + function highlightMatch(text, query) { + if (!text || !query) { + return escapeHtml(text || ''); + } + + var escaped = escapeHtml(text); + var queryEscaped = escapeHtml(query); + var regex = new RegExp('(' + queryEscaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi'); + + return escaped.replace(regex, '$1'); + } + + /** + * Escape HTML entities + */ + function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Initialize when document is ready + $(document).ready(init); + +})(jQuery); diff --git a/src/Admin/AdminController.php b/src/Admin/AdminController.php index c05c151..87a34b1 100644 --- a/src/Admin/AdminController.php +++ b/src/Admin/AdminController.php @@ -52,6 +52,9 @@ final class AdminController // Add to WooCommerce Reports add_filter('woocommerce_admin_reports', [$this, 'addLicenseReports']); + + // AJAX handler for live search + add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']); } /** @@ -108,6 +111,67 @@ final class AdminController [], WC_LICENSED_PRODUCT_VERSION ); + + // Enqueue live search script on licenses page + if ($isLicensePage) { + wp_enqueue_script( + 'wc-licensed-product-admin', + WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/admin-licenses.js', + ['jquery'], + WC_LICENSED_PRODUCT_VERSION, + true + ); + + wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wclp_live_search'), + 'strings' => [ + 'noResults' => __('No licenses found', 'wc-licensed-product'), + 'searching' => __('Searching...', 'wc-licensed-product'), + 'error' => __('Search failed', 'wc-licensed-product'), + ], + ]); + } + } + + /** + * Handle AJAX live search request + */ + public function handleLiveSearch(): void + { + check_ajax_referer('wclp_live_search', 'nonce'); + + if (!current_user_can('manage_woocommerce')) { + wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403); + } + + $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : ''; + + if (strlen($search) < 2) { + wp_send_json_success(['results' => []]); + } + + $filters = ['search' => $search]; + $licenses = $this->licenseManager->getAllLicenses(1, 10, $filters); + + $results = []; + foreach ($licenses as $license) { + $product = wc_get_product($license->getProductId()); + $customer = get_userdata($license->getCustomerId()); + + $results[] = [ + 'id' => $license->getId(), + 'license_key' => $license->getLicenseKey(), + 'domain' => $license->getDomain(), + 'status' => $license->getStatus(), + 'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'), + 'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'), + 'customer_email' => $customer ? $customer->user_email : '', + 'view_url' => admin_url('admin.php?page=wc-licenses&s=' . urlencode($license->getLicenseKey())), + ]; + } + + wp_send_json_success(['results' => $results]); } /**