Skip to content

Instantly share code, notes, and snippets.

@rojenzaman
Last active November 8, 2024 10:54
Show Gist options
  • Save rojenzaman/f7ed44bd966bb9bbf1c35d78cd7d3d8b to your computer and use it in GitHub Desktop.
Save rojenzaman/f7ed44bd966bb9bbf1c35d78cd7d3d8b to your computer and use it in GitHub Desktop.
WordPress Dynamic Data Table Shortcode

WordPress Dynamic Data Table Shortcode

This shortcode generates a paginated, searchable, and sortable data table in WordPress with customizable role-based access control. Ideal for displaying data to specific users with an interactive and secure interface.

Features

  • Search: Supports both global and column-specific searches.
  • Sort: Allows sorting by any column by clicking on headers.
  • Pagination: Includes pagination controls for easy navigation.
  • Access Control: Specify user roles or capabilities with an auth parameter, e.g., [show_data auth="administrator, editor"].

Usage

Insert the shortcode in your WordPress content or template where you want to display the table:

[show_data query="SELECT * FROM your_table WHERE condition" per_page="10" auth="administrator, editor" link_columns="column_2, column_3" link_templates="https://example.com/{{column}}/preview, https://anotherexample.com/view/{{column}}"]

Parameters

  • query: (string) Required. The SQL SELECT query to fetch data.
  • per_page: (integer) Optional. Sets the number of rows per page. Default is 10.
  • auth: (string) Optional. Comma-separated list of roles or capabilities allowed to access the data. Default is administrator.
  • link_columns: (string) Optional. Comma-separated list of columns to display as hyperlinks.
  • link_templates: (string) Optional. Comma-separated list of URL templates for the hyperlinks, matching each column in link_columns. Use {{column}} as a placeholder for the column value.

Example

To display all rows from your_table with 10 rows per page, accessible only to Administrators and Editors, and make column_2 and column_3 values clickable links with different URLs:

[show_data query="SELECT * FROM your_table" per_page="10" auth="administrator, editor" link_columns="column_2, column_3" link_templates="https://example.com/{{column}}/preview, https://anotherexample.com/view/{{column}}"]

Notes

  • Security: This shortcode only accepts SELECT queries for security. All user inputs are sanitized and SQL queries are prepared to prevent SQL injection.
  • Custom Styling: The snippet includes CSS for padding, margin, and responsive layout. Adjust CSS if needed to fit your theme.

This shortcode is perfect for creating interactive data views tailored to specific users in a secure WordPress environment.

function show_data_with_search_and_sorting_shortcode($atts) {
global $wpdb;
// Get the shortcode attributes
$atts = shortcode_atts(array(
'query' => '', // SQL query provided via the 'query' parameter in the shortcode
'per_page' => 10, // Number of rows per page
'auth' => 'administrator', // Roles or capabilities required to view the data
'link_columns' => '', // Columns to be displayed as links
'link_templates' => '', // URL templates for the links, one per column
), $atts, 'show_data');
// Parse the 'auth' attribute into an array of roles/capabilities
$required_roles = array_map('trim', explode(',', $atts['auth']));
// Check if the current user has any of the required roles or capabilities
$user_has_access = false;
$current_user = wp_get_current_user();
foreach ($required_roles as $role) {
if (in_array(strtolower($role), array_map('strtolower', $current_user->roles)) || current_user_can($role)) {
$user_has_access = true;
break;
}
}
// If the user doesn't have access, display a message
if (!$user_has_access) {
return '<p>You do not have permission to view this content.</p>';
}
// If no query is provided, display a message
if (empty($atts['query'])) {
return '<p>No query provided.</p>';
}
// Ensure only SELECT queries are allowed for safety
$query = stripslashes($atts['query']);
if (stripos(trim($query), 'SELECT') !== 0) {
return '<p>Invalid query. Only SELECT queries are allowed.</p>';
}
// Use unique parameter names to avoid conflicts
$pagination_param = 'custom_paged';
$sort_column_param = 'sort_column';
$sort_order_param = 'sort_order';
$search_param = 'search';
$column_search_param = 'col_search';
// Get the current page number from the URL, default to 1 if not set
$current_page = isset($_GET[$pagination_param]) ? max(1, intval($_GET[$pagination_param])) : 1;
$per_page = max(1, intval($atts['per_page']));
$offset = ($current_page - 1) * $per_page;
// Get sort parameters from URL
$sort_column = isset($_GET[$sort_column_param]) ? sanitize_text_field($_GET[$sort_column_param]) : '';
$sort_order = isset($_GET[$sort_order_param]) && strtoupper($_GET[$sort_order_param]) === 'DESC' ? 'DESC' : 'ASC';
// Get search parameters from URL
$global_search = isset($_GET[$search_param]) ? sanitize_text_field($_GET[$search_param]) : '';
$column_search = isset($_GET[$column_search_param]) && is_array($_GET[$column_search_param]) ? array_map('sanitize_text_field', $_GET[$column_search_param]) : array();
// Execute the initial query to get column names
$initial_results = $wpdb->get_results($query . " LIMIT 1");
if (empty($initial_results)) {
return '<p>No records found.</p>';
}
// Get column names from the first row
$columns = array_keys(get_object_vars($initial_results[0]));
// Validate sort column
if (!in_array($sort_column, $columns)) {
$sort_column = ''; // Reset to default if invalid
}
// Build WHERE clauses for search
$where_clauses = array();
$query_params = array();
// Global search across all columns
if ($global_search !== '') {
$global_search_like = '%' . $wpdb->esc_like($global_search) . '%';
$global_search_clauses = array();
foreach ($columns as $column_name) {
$global_search_clauses[] = "`$column_name` LIKE %s";
$query_params[] = $global_search_like;
}
$where_clauses[] = '(' . implode(' OR ', $global_search_clauses) . ')';
}
// Column-specific searches
foreach ($columns as $column_name) {
if (isset($column_search[$column_name]) && $column_search[$column_name] !== '') {
$search_value = '%' . $wpdb->esc_like($column_search[$column_name]) . '%';
$where_clauses[] = "`$column_name` LIKE %s";
$query_params[] = $search_value;
}
}
// Build the final WHERE clause
$where_clause = '';
if (!empty($where_clauses)) {
$where_clause = ' WHERE ' . implode(' AND ', $where_clauses);
}
// Build ORDER BY clause if sorting is applied
$order_by_clause = '';
if ($sort_column) {
// Ensure the column name is safe (whitelisted)
$order_by_clause = " ORDER BY `" . esc_sql($sort_column) . "` $sort_order";
}
// Modify the query to add WHERE, ORDER BY, and LIMIT for pagination
$paged_query = "SELECT * FROM (" . $query . ") AS subquery" . $where_clause . $order_by_clause . " LIMIT %d OFFSET %d";
// Prepare the paged query
$prepared_query = $wpdb->prepare($paged_query, array_merge($query_params, array($per_page, $offset)));
// Execute the paged query
$results = $wpdb->get_results($prepared_query);
// Count total records for pagination
$count_query = "SELECT COUNT(*) FROM (" . $query . ") AS subquery" . $where_clause;
$count_prepared_query = $wpdb->prepare($count_query, $query_params);
$total_records = $wpdb->get_var($count_prepared_query);
$total_pages = ceil($total_records / $per_page);
// Parse link columns and templates
$link_columns = array_map('trim', explode(',', $atts['link_columns']));
$link_templates = array_map('trim', explode(',', $atts['link_templates']));
// Create an associative array mapping columns to their templates
$link_columns_templates = array();
foreach ($link_columns as $index => $column_name) {
$template = isset($link_templates[$index]) ? $link_templates[$index] : '';
$link_columns_templates[$column_name] = $template;
}
// Start output buffering to capture the output
ob_start();
// Include CSS and JavaScript for enhanced styling and interactivity
?>
<!-- Custom Styles -->
<style>
.custom-data-table-container {
margin: 20px;
padding: 20px;
border: 1px solid #ddd;
font-family: Arial, sans-serif;
}
.custom-search-form {
margin-bottom: 20px;
}
.custom-search-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.custom-search-form input[type="text"] {
padding: 8px;
margin-bottom: 10px;
width: 100%;
box-sizing: border-box;
}
.custom-search-form button {
padding: 8px 16px;
margin-top: 10px;
margin-right: 10px;
}
.custom-search-row {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.custom-search-column {
flex: 1;
min-width: 200px;
}
.custom-data-table {
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}
.custom-data-table th,
.custom-data-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.custom-data-table th {
background-color: #f2f2f2;
position: relative;
cursor: pointer;
}
.custom-data-table th .sort-indicator {
position: absolute;
right: 8px;
}
.custom-data-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.custom-pagination {
text-align: center;
margin-top: 20px;
}
.custom-pagination a,
.custom-pagination span {
margin: 0 5px;
text-decoration: none;
}
.custom-pagination a {
color: #0073aa;
}
.custom-pagination a:hover {
text-decoration: underline;
}
/* Padding and margin adjustments for search boxes */
.custom-global-search {
margin-top: 20px;
margin-left: 20px;
margin-right: 20px;
margin-bottom: 20px;
}
.custom-global-search input[type="text"] {
padding: 8px;
width: 300px;
max-width: 100%;
box-sizing: border-box;
margin-right: 10px;
}
.custom-global-search button {
padding: 8px 16px;
margin-top: 0;
margin-right: 10px;
}
.custom-column-search input[type="text"] {
padding: 6px;
width: 100%;
box-sizing: border-box;
margin-top: 5px;
}
</style>
<!-- Custom JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Add event listeners to table headers for sorting
var headers = document.querySelectorAll('.custom-data-table th.sortable');
headers.forEach(function (header) {
header.addEventListener('click', function () {
window.location.href = this.dataset.sortUrl;
});
});
// Clear search inputs when 'Clear' button is clicked
var clearButton = document.getElementById('clear_search');
if (clearButton) {
clearButton.addEventListener('click', function () {
// Clear global search input
var globalSearchInput = document.getElementById('global_search');
if (globalSearchInput) {
globalSearchInput.value = '';
}
// Clear column-specific search inputs
var columnSearchInputs = document.querySelectorAll('.custom-column-search input[type="text"]');
columnSearchInputs.forEach(function (input) {
input.value = '';
});
// Submit the form
this.form.submit();
});
}
});
</script>
<?php
// Build the search form
echo '<form method="get" class="custom-search-form">';
// Preserve other query parameters
foreach ($_GET as $key => $value) {
if (!in_array($key, array($pagination_param, $search_param, $column_search_param))) {
if (is_array($value)) {
foreach ($value as $array_value) {
echo '<input type="hidden" name="' . esc_attr($key) . '[]" value="' . esc_attr($array_value) . '">';
}
} else {
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr($value) . '">';
}
}
}
// Global search field
echo '<div class="custom-global-search">';
echo '<label for="global_search">Search All Columns:</label>';
echo '<input type="text" id="global_search" name="' . esc_attr($search_param) . '" value="' . esc_attr($global_search) . '">';
echo '<button type="submit">Search</button>';
echo '<button type="button" id="clear_search">Clear</button>';
echo '</div>';
// Start the table
echo '<div class="custom-data-table-container">';
echo '<table class="custom-data-table">';
// Column-specific search inputs (placed above the headers)
echo '<tr>';
foreach ($columns as $column_name) {
echo '<td class="custom-column-search">';
$search_value = isset($column_search[$column_name]) ? $column_search[$column_name] : '';
echo '<input type="text" name="' . esc_attr($column_search_param) . '[' . esc_attr($column_name) . ']" value="' . esc_attr($search_value) . '">';
echo '</td>';
}
echo '</tr>';
// Table headers with sorting links
echo '<tr>';
// Build base URL for sorting links
$base_url = get_permalink();
// Preserve existing query parameters except sort and pagination parameters
$query_args = $_GET;
unset($query_args[$sort_column_param]);
unset($query_args[$sort_order_param]);
unset($query_args[$pagination_param]);
foreach ($columns as $column_name) {
// Determine the sort order for this column
$new_sort_order = 'ASC';
if ($sort_column === $column_name && $sort_order === 'ASC') {
$new_sort_order = 'DESC';
}
// Build the sorting URL
$query_args[$sort_column_param] = $column_name;
$query_args[$sort_order_param] = $new_sort_order;
$sorting_url = add_query_arg($query_args, $base_url);
// Add sort indicators
$sort_indicator = '';
if ($sort_column === $column_name) {
$sort_indicator = $sort_order === 'ASC' ? '▲' : '▼';
}
echo '<th class="sortable" data-sort-url="' . esc_url($sorting_url) . '">';
echo '<span>' . esc_html($column_name) . '</span>';
if ($sort_indicator) {
echo '<span class="sort-indicator">' . $sort_indicator . '</span>';
}
echo '</th>';
}
echo '</tr>';
// Close the form tag here so the table is submitted when search inputs are used
echo '</form>';
// Add table rows
if (!empty($results)) {
foreach ($results as $row) {
echo '<tr>';
foreach ($columns as $column_name) {
$cell_value = esc_html($row->$column_name);
// Check if the column should be displayed as a link
if (isset($link_columns_templates[$column_name]) && !empty($link_columns_templates[$column_name])) {
$template = $link_columns_templates[$column_name];
$url = str_replace('{{column}}', urlencode($row->$column_name), $template);
$cell_value = '<a href="' . esc_url($url) . '" target="_blank">' . $cell_value . '</a>';
}
echo '<td>' . $cell_value . '</td>';
}
echo '</tr>';
}
} else {
echo '<tr><td colspan="' . count($columns) . '">No records found.</td></tr>';
}
echo '</table>';
// Pagination controls
if ($total_pages > 1) {
echo '<div class="custom-pagination">';
// Build base URL for pagination links
$query_args = $_GET;
unset($query_args[$pagination_param]);
// Determine the range of pages to display
$max_display_pages = 7;
$half = floor($max_display_pages / 2);
$start_page = max(1, $current_page - $half);
$end_page = min($total_pages, $current_page + $half);
if ($current_page <= $half) {
$end_page = min($total_pages, $max_display_pages);
}
if ($current_page + $half >= $total_pages) {
$start_page = max(1, $total_pages - $max_display_pages + 1);
}
// "First" and "Previous" links
if ($current_page > 1) {
$query_args[$pagination_param] = 1;
$first_page_url = add_query_arg($query_args, $base_url);
echo '<a href="' . esc_url($first_page_url) . '">&laquo; First</a>';
$query_args[$pagination_param] = $current_page - 1;
$prev_page_url = add_query_arg($query_args, $base_url);
echo '<a href="' . esc_url($prev_page_url) . '">&lsaquo; Prev</a>';
}
// Ellipsis before
if ($start_page > 1) {
echo '<span>...</span>';
}
// Page number links
for ($i = $start_page; $i <= $end_page; $i++) {
$query_args[$pagination_param] = $i;
$page_url = add_query_arg($query_args, $base_url);
if ($i == $current_page) {
echo "<span>$i</span>";
} else {
echo '<a href="' . esc_url($page_url) . '">' . $i . '</a>';
}
}
// Ellipsis after
if ($end_page < $total_pages) {
echo '<span>...</span>';
}
// "Next" and "Last" links
if ($current_page < $total_pages) {
$query_args[$pagination_param] = $current_page + 1;
$next_page_url = add_query_arg($query_args, $base_url);
echo '<a href="' . esc_url($next_page_url) . '">Next &rsaquo;</a>';
$query_args[$pagination_param] = $total_pages;
$last_page_url = add_query_arg($query_args, $base_url);
echo '<a href="' . esc_url($last_page_url) . '">Last &raquo;</a>';
}
echo '</div>';
}
echo '</div>'; // Close the data table container
// Return the buffered content
return ob_get_clean();
}
add_shortcode('show_data', 'show_data_with_search_and_sorting_shortcode');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment