/**
* Smart Tables - Manage Records Implementation
* ---------------------------------------------
* This script implements a complete data management UI for handling CRUD operations
* on tabular data using the SmartTables library.
*
* Key Features:
* - Client-side data manipulation with full CRUD operations
* - Row state tracking (editing, saved states with visual feedback)
* - Custom responsive behavior
* - Modal-based editing with form validation
* - Pagination, sorting, and searching
* - Bootstrap integration for modern UI components
*
* Data Flow:
* 1. Initial data is loaded from the sampleData array
* 2. CRUD operations update both the UI and the underlying data source
* 3. Hooks provide extension points for customizing behavior
* 4. Event listeners manage user interactions
*
* Dependencies:
* - SmartTables library (../smartTables.bundle.js)
* - Bootstrap 5.x (for modal dialogs and UI components)
*
* @author Sunnyat A. (SmartAdmin Template Team)
* @license WrapBootstrap
*/
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
/**
* Sample Data
* -----------
* This data represents a collection of employee records that will be used to populate the table.
* In a production environment, this would typically come from an API or database.
*
* Data Structure:
* - id: Unique identifier for each record
* - name: Employee full name
* - email: Employee email address
* - phone: Contact phone number with formatting
* - salary: Numeric value representing annual salary
* - department: Department name (used for filtering/grouping)
* - position: Job title
* - joinDate: ISO date string representing hire date
* - active: Boolean indicating current employment status
* - status: Employment type (Full-time, Part-time, Contract, Intern)
*
* Note: IDs are converted to strings during initialization to ensure consistent comparison
* with string values that might come from form inputs or URL parameters.
*/
const sampleData = [
{
id: 146134,
name: 'John Doe',
email: 'john@example.com',
phone: '(555) 123-4567',
salary: 75000,
department: 'Engineering',
position: 'Senior Developer',
joinDate: '2020-03-15',
active: true,
status: 'Full-time'
},
{
id: 146137,
name: 'Jane Smith',
email: 'jane@example.com',
phone: '(555) 987-6543',
salary: 82000,
department: 'Marketing',
position: 'Marketing Manager',
joinDate: '2019-07-22',
active: true,
status: 'Full-time'
},
{
id: 146142,
name: 'Bob Johnson',
email: 'bob@example.com',
phone: '(555) 234-5678',
salary: 65000,
department: 'Customer Support',
position: 'Support Specialist',
joinDate: '2021-01-10',
active: false,
status: 'Part-time'
},
{
id: 146145,
name: 'Sarah Williams',
email: 'sarah@example.com',
phone: '(555) 345-6789',
salary: 90000,
department: 'Human Resources',
position: 'HR Director',
joinDate: '2018-11-05',
active: true,
status: 'Full-time'
},
{
id: 146147,
name: 'Mike Davis',
email: 'mike@example.com',
phone: '(555) 456-7890',
salary: 67500,
department: 'Engineering',
position: 'Junior Developer',
joinDate: '2022-02-28',
active: true,
status: 'Contract'
},
{
id: 146152,
name: 'Emily Wilson',
email: 'emily@example.com',
phone: '(555) 567-8901',
salary: 72000,
department: 'Finance',
position: 'Financial Analyst',
joinDate: '2020-09-14',
active: true,
status: 'Full-time'
},
{
id: 146156,
name: 'David Brown',
email: 'david@example.com',
phone: '(555) 678-9012',
salary: 85000,
department: 'Engineering',
position: 'DevOps Engineer',
joinDate: '2019-05-18',
active: true,
status: 'Full-time'
},
{
id: 146161,
name: 'Lisa Taylor',
email: 'lisa@example.com',
phone: '(555) 789-0123',
salary: 58000,
department: 'Sales',
position: 'Sales Representative',
joinDate: '2021-11-30',
active: false,
status: 'Part-time'
},
{
id: 146165,
name: 'James Miller',
email: 'james@example.com',
phone: '(555) 890-1234',
salary: 95000,
department: 'Executive',
position: 'CTO',
joinDate: '2017-08-12',
active: true,
status: 'Full-time'
},
{
id: 146169,
name: 'Amanda Clark',
email: 'amanda@example.com',
phone: '(555) 901-2345',
salary: 78000,
department: 'Marketing',
position: 'Content Strategist',
joinDate: '2020-06-25',
active: true,
status: 'Full-time'
},
{
id: 146172,
name: 'Robert White',
email: 'robert@example.com',
phone: '(555) 012-3456',
salary: 62000,
department: 'Customer Support',
position: 'Technical Support',
joinDate: '2022-04-05',
active: true,
status: 'Contract'
},
{
id: 146175,
name: 'Jennifer Lee',
email: 'jennifer@example.com',
phone: '(555) 123-7890',
salary: 88000,
department: 'Product',
position: 'Product Manager',
joinDate: '2019-02-17',
active: true,
status: 'Full-time'
},
{
id: 146178,
name: 'Thomas Harris',
email: 'thomas@example.com',
phone: '(555) 234-8901',
salary: 71000,
department: 'Engineering',
position: 'QA Engineer',
joinDate: '2021-07-08',
active: true,
status: 'Full-time'
},
{
id: 146181,
name: 'Patricia Martin',
email: 'patricia@example.com',
phone: '(555) 345-9012',
salary: 83000,
department: 'Human Resources',
position: 'Recruitment Manager',
joinDate: '2018-04-22',
active: false,
status: 'Contract'
},
{
id: 146184,
name: 'Daniel Anderson',
email: 'daniel@example.com',
phone: '(555) 456-0123',
salary: 69000,
department: 'Finance',
position: 'Accountant',
joinDate: '2020-12-03',
active: true,
status: 'Full-time'
},
{
id: 146187,
name: 'Michelle Walker',
email: 'michelle@example.com',
phone: '(555) 567-1234',
salary: 77000,
department: 'Sales',
position: 'Sales Manager',
joinDate: '2019-10-19',
active: true,
status: 'Full-time'
},
{
id: 146190,
name: 'Christopher Young',
email: 'chris@example.com',
phone: '(555) 678-2345',
salary: 92000,
department: 'Engineering',
position: 'Data Scientist',
joinDate: '2018-07-11',
active: true,
status: 'Full-time'
},
{
id: 146193,
name: 'Nancy Hall',
email: 'nancy@example.com',
phone: '(555) 789-3456',
salary: 64000,
department: 'Marketing',
position: 'Social Media Specialist',
joinDate: '2021-05-27',
active: true,
status: 'Part-time'
},
{
id: 146196,
name: 'Kevin Allen',
email: 'kevin@example.com',
phone: '(555) 890-4567',
salary: 87000,
department: 'Product',
position: 'UX Designer',
joinDate: '2020-01-14',
active: true,
status: 'Full-time'
},
{
id: 146199,
name: 'Laura King',
email: 'laura@example.com',
phone: '(555) 901-5678',
salary: 73000,
department: 'Customer Support',
position: 'Customer Success Manager',
joinDate: '2019-09-08',
active: false,
status: 'Contract'
}
];
// Convert IDs to strings during initialization
sampleData.forEach(item => {
item.id = String(item.id);
});
/**
* Table Initialization
* --------------------
* This section initializes the SmartTables instance when the DOM is fully loaded.
* It configures all the columns, data types, and event handling for the table.
*/
document.addEventListener('DOMContentLoaded', () => {
/**
* Row State Management
* -------------------
* This object tracks the state of rows across various operations.
*
* Properties:
* - editing: Stores the ID of the row currently being edited (null when no editing in progress)
* - saved: A Set containing IDs of rows that were recently saved (for visual feedback)
* - adding: Boolean flag indicating if a new record is being added
* - deleting: Stores the ID of the row currently being deleted (null when no deletion in progress)
*
* This state management allows for visual indicators and proper cleanup across operations.
*/
const rowStates = {
editing: null, // ID of the row currently being edited
saved: new Set(), // Set of row IDs that should have the 'saved' class
adding: false, // Flag to track when a new record is being added
deleting: null // ID of the row currently being deleted
};
/**
* SmartTables Instance
* -------------------
* This is the main table instance that powers the data management interface.
* It's configured with columns, data types, and hooks for various operations.
*
* @type {SmartTables}
*/
const clientTable = new SmartTables('clientTable', {
/**
* Data Configuration
* -----------------
* Defines the data source and column structure for the table.
*/
data: {
type: 'json', // Data format is JSON
source: sampleData, // Use the sample data array as the data source
idField: 'id', // Field that uniquely identifies each record
columns: [
{
data: 'id', // Field name in the data object
title: 'ID', // Column header text
editable: false // ID cannot be edited
},
{
data: 'name',
title: 'Name',
required: true // Field is required in the edit form
},
{
data: 'email',
title: 'Email',
type: 'email', // Email input type for validation
required: true
},
{
data: 'phone',
title: 'Phone',
type: 'tel', // Telephone input type
format: 'phone', // Custom formatting for display
placeholder: '(555) 555-5555',
required: true
},
{
data: 'salary',
title: 'Salary',
type: 'number', // Numeric input type
min: 0, // Minimum value
step: 500, // Increment by 500
render: data => '$' + data.toLocaleString() // Format with $ and commas
},
{
data: 'department',
title: 'Department',
type: 'select', // Dropdown select input
options: [ // Available options
'Engineering',
'Sales',
'Executive',
'Marketing',
'Human Resources',
'Customer Support',
'Finance',
'Operations',
'Product',
'Research',
'Legal',
'IT'
]
},
{
data: 'joinDate',
title: 'Join Date',
type: 'date', // Date input type
render: data => new Date(data).toLocaleDateString() // Format as local date
},
{
data: 'active',
title: 'Active',
type: 'boolean', // Boolean/checkbox input type
render: data => data === true
? 'Yes' // Visual indicator for active
: 'No' // Visual indicator for inactive
},
{
data: 'status',
title: 'Status',
type: 'select',
options: [ // Options with separate value and label
{ value: 'Full-time', label: 'Full-time' },
{ value: 'Part-time', label: 'Part-time' },
{ value: 'Contract', label: 'Contract' },
{ value: 'Intern', label: 'Intern' }
]
},
{
data: 'actions', // Special column for action buttons
title: 'Actions',
sortable: false, // Don't allow sorting by actions
editable: false, // Not included in edit form
render: (data, row) => `
`
}
]
},
debug: true, // Enable debug logging
responsive: true, // Enable responsive behavior
addRecord: true, // Enable the built-in Add Record button
/**
* Hooks Configuration
* ------------------
* Hooks provide extension points for customizing behavior at different stages
* of the table's lifecycle and operations.
*/
hooks: {
/**
* Before Edit Hook
* ---------------
* Called before a row enters edit mode.
* Sets up visual feedback by adding the 'editing' class.
*
* @param {string} rowId - ID of the row being edited
* @returns {boolean} - Whether to proceed with the edit operation
*/
beforeEdit(rowId) {
// Set the editing state
rowStates.editing = rowId;
// Apply the editing class
const rowElement = this.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.add('editing');
}
return true; // Allow the edit to proceed
},
/**
* After Edit Hook
* --------------
* Called after an edit operation completes (success or failure).
* Handles visual feedback and table redrawing.
*
* @param {Object|string} rowId - Row ID or object containing edit result
* @param {Object} [rowData] - Data of the edited row
* @param {boolean} [success] - Whether the edit was successful
*/
afterEdit(...args) {
// Handle different possible argument signatures
let rowId, rowData, success;
if (args.length === 1 && typeof args[0] === 'object') {
({ rowId, rowData, success } = args[0]);
} else {
[rowId, rowData, success] = args;
}
// Clear editing state
rowStates.editing = null;
// If the edit was successful, mark the row as saved
if (success === true) {
rowStates.saved.add(rowId);
setTimeout(() => {
rowStates.saved.delete(rowId);
// Reapply classes after timeout
const rowElement = this.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.remove('saved');
}
}, 3000);
}
// Redraw the table to reflect updated data
this.draw();
// Reapply any active search
if (this.searchQuery && this.searchQuery.trim() !== '') {
this.handleSearch(this.searchQuery);
}
// Reapply any active column sorting
if (this.currentSortColumn !== undefined && this.currentSortDirection) {
this.sortBy(this.currentSortColumn, this.currentSortDirection);
}
// Reapply classes after redraw
if (rowStates.editing) {
const editingRow = this.table.querySelector(`tbody tr[data-id="${rowStates.editing}"]`);
if (editingRow) {
editingRow.classList.add('editing');
}
}
rowStates.saved.forEach(savedRowId => {
const savedRow = this.table.querySelector(`tbody tr[data-id="${savedRowId}"]`);
if (savedRow) {
savedRow.classList.add('saved');
}
});
},
/**
* Edit Modal Created Hook
* ----------------------
* Called when the edit modal HTML is created.
* Allows customization of the modal HTML before it's added to the DOM.
*
* @param {string} modalHTML - Generated HTML for the modal
* @param {string} rowId - ID of the row being edited
* @param {Object} rowData - Data of the row being edited
* @returns {string} - Original or modified HTML for the modal
*/
onEditModalCreated(modalHTML, rowId, rowData) {
// You could customize the modal HTML here
// For example, add custom fields, change styling, etc.
return modalHTML; // Return original or modified HTML
},
/**
* Edit Modal Before Show Hook
* --------------------------
* Called just before the edit modal is shown.
* Allows adding event listeners, initializing third-party components, etc.
*
* @param {HTMLElement} modalElement - The modal DOM element
* @param {string} rowId - ID of the row being edited
* @param {Object} rowData - Data of the row being edited
*/
onEditModalBeforeShow(modalElement, rowId, rowData) {
// Add custom event listeners, initialize third-party components
console.log('Edit modal is about to be shown for row:', rowId);
// Example: Add custom class to modal
modalElement.classList.add('custom-edit-modal');
// Example: Add custom event listener
const form = modalElement.querySelector('form');
if (form) {
form.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent default form submission on Enter
}
});
}
},
/**
* Edit Data Collected Hook
* -----------------------
* Called after form data is collected but before it's applied to the table.
* Allows transformation and validation of the data.
*
* @param {Object} updatedData - Data collected from the edit form
* @param {string} rowId - ID of the row being edited
* @param {Object} originalData - Original data of the row before editing
* @returns {Object} - Processed data to be applied to the table
*/
onEditDataCollected(updatedData, rowId, originalData) {
// Process data before it's applied to the table
console.log('Data collected from edit form:', updatedData);
const processedData = { ...updatedData };
// Map column data fields to their types for easy lookup
const columnTypes = {};
this.options.data.columns.forEach(column => {
if (column.data && column.type) {
columnTypes[column.data] = column.type;
}
});
// Process each field in updatedData based on its column type
for (const field in columnTypes) {
const type = columnTypes[field];
const value = processedData[field];
switch (type) {
case 'number':
// Convert to number, default to 0 if invalid
processedData[field] = value !== undefined && value !== ''
? parseFloat(value)
: 0;
break;
case 'boolean':
// Convert to boolean: true if field is present and "on", false if not present
processedData[field] = field in processedData && value === 'on';
break;
case 'date':
// Ensure the value is a valid date string (or convert to Date object if needed)
processedData[field] = value ? new Date(value).toISOString().split('T')[0] : '';
break;
case 'text':
case 'select':
// Keep as string, no conversion needed
break;
default:
// No type specified, leave as-is
break;
}
}
// Example: Add timestamp for when the record was modified
processedData.lastModified = new Date().toISOString();
// Update sampleData directly using the hook
const recordIndex = sampleData.findIndex(item => item.id === rowId);
if (recordIndex !== -1) {
sampleData[recordIndex] = { ...sampleData[recordIndex], ...processedData };
}
return processedData;
},
/**
* Edit Success Hook
* ---------------
* Called when a record is successfully updated.
* Handles success notifications and UI updates.
*
* @param {string} rowId - ID of the edited row
* @param {Object} updatedRecord - The updated record data
* @param {Object} submittedData - The data that was submitted
*/
onEditSuccess(rowId, updatedRecord, submittedData) {
// Handle successful edit
console.log('Record updated successfully:', rowId);
// Example: Show custom notification
const notification = document.createElement('div');
notification.className = 'floating-notification success';
notification.textContent = `Record ${rowId} updated successfully`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 2000);
}, 10);
},
/**
* Edit Error Hook
* -------------
* Called when an error occurs during record update.
* Handles error notifications and recovery.
*
* @param {string} rowId - ID of the row that failed to update
* @param {Error} error - The error that occurred
* @param {Object} attemptedData - The data that was attempted to be submitted
*/
onEditError(rowId, error, attemptedData) {
// Handle edit error
console.error('Error updating record:', rowId, error);
},
/**
* Before Delete Hook
* ----------------
* Called before a row is deleted.
* Sets up visual feedback and can abort the deletion.
*
* @param {string} rowId - ID of the row to be deleted
* @returns {boolean} - Whether to proceed with the deletion
*/
beforeDelete(rowId) {
// Set the deleting state and apply visual feedback
rowStates.deleting = rowId;
const rowElement = this.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.add('deleting');
}
return true; // Allow the deletion to proceed
},
/**
* After Delete Hook
* ---------------
* Called after a deletion attempt completes (success or failure).
* Handles cleanup and visual feedback.
*
* @param {string} rowId - ID of the deleted (or attempted) row
* @param {Object} data - Response data, if any
* @param {boolean} success - Whether the deletion was successful
*/
afterDelete(rowId, data, success) {
// Clear the deleting state
rowStates.deleting = null;
// Handle deletion result
if (success) {
console.log('Record deleted successfully:', rowId);
//draw the table to fix pagination
this.draw();
// Reapply any active column sorting
if (this.currentSortColumn !== undefined && this.currentSortDirection) {
this.sortBy(this.currentSortColumn, this.currentSortDirection);
}
} else {
// Clear deleting state on failure
const rowElement = this.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.remove('deleting');
}
}
},
onDeleteModalCreated(modalHtml, rowId) {
// Customize delete confirmation modal
return modalHtml; // Return original or modified HTML
},
/**
* Delete Success Hook
* -----------------
* Called when a record is successfully deleted.
* Handles data updates and notifications.
*
* @param {string} rowId - ID of the deleted row
* @param {Object} deletedRecord - The data of the deleted record
*/
onDeleteSuccess(rowId, deletedRecord) {
// Handle successful deletion
console.log('Record deleted:', rowId);
// Note: Data updates are now handled in the click handler
},
/**
* Before Add Record Hook
* ---------------------
* Called before a new record form is displayed.
* Sets up visual feedback and initializes default values.
*
* @param {Object} initialData - Initial data for the new record
* @param {Object} options - Additional options for the add operation
* @returns {Object|boolean} - Modified initial data or false to abort
*/
beforeAddRecord(initialData, options) {
// Set the adding state
rowStates.adding = true;
// Log the operation
console.log('Adding new record with initial data:', initialData);
// Return the initial data (potentially modified)
return initialData;
},
/**
* After Add Record Hook
* --------------------
* Called after a record add operation completes (success or failure).
* Handles cleanup and visual feedback.
*
* @param {Object} newRecordData - The data of the new record
* @param {boolean} success - Whether the operation was successful
*/
afterAddRecord(newRecordData, success) {
// Reset the adding state
rowStates.adding = false;
if (success) {
console.log('Successfully added new record:', newRecordData);
// Add the new record to our local data source (if not already done by the library)
// This ensures our local data stays in sync with the table
const recordExists = sampleData.some(item => item.id === newRecordData.id);
if (!recordExists) {
sampleData.push(newRecordData);
}
} else {
console.warn('Failed to add new record');
}
},
/**
* Add Modal Created Hook
* ---------------------
* Called when the add record modal is created.
* Allows customization of the modal HTML.
*
* @param {string} modalHTML - The default modal HTML
* @param {Object} initialData - Initial data for the new record
* @param {Object} options - Additional options for the add operation
* @returns {string} - Customized modal HTML
*/
onAddModalCreated(modalHTML, initialData, options) {
// Parse the modal HTML to a DOM element for easier manipulation
const parser = new DOMParser();
const modalDoc = parser.parseFromString(modalHTML, 'text/html');
const modalElement = modalDoc.body.firstChild;
// Add custom classes
modalElement.classList.add('smarttables-add-modal');
// Customize the modal header with additional styling
// const modalHeader = modalElement.querySelector('.modal-header');
// if (modalHeader) {
// modalHeader.classList.add('bg-primary', 'text-white');
// }
// Customize the submit button
const submitButton = modalElement.querySelector('[id$="-submit"]');
if (submitButton) {
submitButton.classList.add('btn-success');
// Add an icon to the button
submitButton.innerHTML = ` ${submitButton.textContent}`;
}
// Convert the modified modal back to HTML string
return modalElement.outerHTML;
},
/**
* Add Modal Before Show Hook
* -------------------------
* Called right before the add modal is shown.
* Use for last-minute DOM manipulations.
*
* @param {HTMLElement} modalElement - The modal DOM element
* @param {Object} initialData - Initial data for the new record
* @param {Object} options - Additional options
*/
onAddModalBeforeShow(modalElement, initialData, options) {
// Add animation class
modalElement.classList.add('fade');
// Focus the first input when the modal is shown
modalElement.addEventListener('shown.bs.modal', () => {
const firstInput = modalElement.querySelector('form input:not([type="hidden"]):not([readonly]):not([disabled])');
if (firstInput) {
firstInput.focus();
}
});
},
/**
* Add Data Collected Hook
* ----------------------
* Called after the form data is collected but before submission.
* Allows for data transformation and validation.
*
* @param {Object} newRecordData - The collected form data
* @param {Object} options - Additional options
* @returns {Object} - Transformed data to be submitted
*/
onAddDataCollected(newRecordData, options) {
// Transform any data if needed before submission
// For example, ensure salary is a number
if (newRecordData.salary) {
newRecordData.salary = Number(newRecordData.salary);
}
// Set boolean values correctly
if (newRecordData.active === 'on' || newRecordData.active === 'true') {
newRecordData.active = true;
} else if (newRecordData.active === 'off' || newRecordData.active === 'false' || newRecordData.active === undefined) {
newRecordData.active = false;
}
// Generate a unique ID if not provided
if (!newRecordData.id) {
// Get highest ID and increment
const highestId = Math.max(...sampleData.map(item => parseInt(item.id)));
newRecordData.id = String(highestId + 1);
}
return newRecordData;
},
/**
* Add Record Success Hook
* ----------------------
* Called when a record is successfully added.
* Handles UI updates and additional processing.
*
* @param {Object} newRecord - The newly added record
*/
onAddRecordSuccess(newRecord) {
console.log('Record added successfully:', newRecord);
// Draw the table to fix pagination
this.draw();
// Reapply any active search query
if (this.searchQuery && this.searchQuery.trim() !== '') {
setTimeout(() => this.handleSearch(this.searchQuery), 50);
}
// Reapply any active column sorting
if (this.currentSortColumn !== undefined && this.currentSortDirection) {
setTimeout(() => this.sortBy(this.currentSortColumn, this.currentSortDirection), 100);
}
// Add visual feedback for the new row
setTimeout(() => {
const newRow = clientTable.table.querySelector(`tbody tr[data-id="${newRecord.id}"]`);
if (newRow) {
newRow.classList.add('new-record');
// Remove class after animation completes
setTimeout(() => {
newRow.classList.remove('new-record');
}, 3000);
}
}, 200);
},
/**
* Add Record Error Hook
* -------------------
* Called when an error occurs during record addition.
* Handles error reporting and recovery.
*
* @param {Error} error - The error that occurred
* @param {Object} attemptedData - The data that failed to be added
*/
onAddRecordError(error, attemptedData) {
console.error('Error adding record:', error);
console.error('Attempted data:', attemptedData);
// Display error in UI if needed
// This supplements the notification already shown by the library
},
/**
* Add Cancelled Hook
* -----------------
* Called when the add operation is cancelled.
*
* @param {Object} options - Options that were passed to addRecord
*/
onAddCancelled(options) {
console.log('Add operation cancelled');
rowStates.adding = false;
},
/**
* After Init Hook
* --------------
* Called after the table is fully initialized.
* Use to add custom UI elements or attach event handlers.
*/
afterInit() {
// Add a button to the toolbar to trigger adding a new record
const toolbar = this.toolbarElement;
if (toolbar) {
// Create the Add Record button
const addButton = document.createElement('button');
addButton.className = 'btn btn-success add-record-btn ms-2';
addButton.innerHTML = ' Add Record';
addButton.setAttribute('type', 'button');
// Find the toolbar's button group or create one
let buttonGroup = toolbar.querySelector('.btn-group');
if (!buttonGroup) {
buttonGroup = document.createElement('div');
buttonGroup.className = 'btn-group ms-2';
toolbar.appendChild(buttonGroup);
} else {
// If button group exists, add margin
buttonGroup.classList.add('ms-2');
}
// Add button to toolbar
toolbar.insertBefore(addButton, buttonGroup);
// Attach event listener
addButton.addEventListener('click', this.handleAddRecord.bind(this));
}
},
}
});
/**
* Add Record Handler
* ----------------
* Method to handle adding a new record.
* Attached to the Add Record button.
*/
SmartTables.prototype.handleAddRecord = function() {
if (rowStates.adding || rowStates.editing) {
// Prevent multiple add/edit operations simultaneously
console.warn('Cannot add record while another operation is in progress');
this.showNotification('Please complete the current operation first', 'warning');
return;
}
// Call the addRecord method with default empty data
this.addRecord({}, {
showNotifications: true,
title: 'Add New Employee',
submitButtonText: 'Create Employee',
cancelButtonText: 'Cancel'
}).catch(error => {
console.error('Error during add operation:', error);
rowStates.adding = false;
});
};
/**
* Event Delegation
* ---------------
* Uses event delegation to handle all button clicks within the table.
* This is more efficient than attaching individual handlers to each button.
*/
clientTable.table.addEventListener('click', e => {
if (e.target.classList.contains('edit-btn')) {
const rowId = e.target.getAttribute('data-id');
clientTable.edit(rowId).catch(error => {
console.error('Error during edit operation:', error);
rowStates.editing = null;
});
} else if (e.target.classList.contains('delete-btn')) {
const rowId = e.target.getAttribute('data-id');
clientTable.delete(rowId)
.then((result) => {
// Check if the delete operation was successful or cancelled
if (result === true) {
// After successful delete, force redraw with updated data
// This ensures pagination is correctly recalculated
const recordIndex = sampleData.findIndex(item => item.id === rowId);
if (recordIndex !== -1) {
sampleData.splice(recordIndex, 1);
clientTable.options.data.source = sampleData;
clientTable.updateTableData(sampleData);
// Reapply any active column sorting
if (clientTable.currentSortColumn !== undefined && clientTable.currentSortDirection) {
clientTable.sortBy(clientTable.currentSortColumn, clientTable.currentSortDirection);
}
// Note: handleSearch is already called in updateTableData if there's an active search
}
} else if (result === false) {
// Delete operation was cancelled by user
// Clean up the 'deleting' class that was added in beforeDelete
const rowElement = clientTable.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.remove('deleting');
}
console.log('Delete operation cancelled for row:', rowId);
}
})
.catch(error => {
console.error('Error during delete operation:', error);
// Also clean up the 'deleting' class on error
const rowElement = clientTable.table.querySelector(`tbody tr[data-id="${rowId}"]`);
if (rowElement) {
rowElement.classList.remove('deleting');
}
});
}
});
// Add an event listener for the custom add button
document.addEventListener('click', e => {
if (e.target.closest('.add-record-btn')) {
// Call the handler method on the clientTable instance
clientTable.handleAddRecord();
}
});
/**
* Modal Close Event Handler
* ------------------------
* Event listener for Bootstrap modal hidden event.
* Cleans up row states and classes when modals are closed.
*
* This ensures proper visual state is maintained even if the user
* cancels an edit operation by closing the modal.
*/
document.addEventListener('hidden.bs.modal', (event) => {
// Check if this is a delete modal by looking at the modal ID
const modal = event.target;
const isDeleteModal = modal && modal.id && modal.id.startsWith('st-delete-modal-');
if (isDeleteModal && rowStates.deleting) {
// Extract row ID from modal ID (format: st-delete-modal-{rowId})
const modalRowId = modal.id.replace('st-delete-modal-', '');
if (modalRowId === rowStates.deleting) {
// Clean up the deleting class since the modal was closed without confirmation
const rowElement = clientTable.table.querySelector(`tbody tr[data-id="${rowStates.deleting}"]`);
if (rowElement) {
rowElement.classList.remove('deleting');
}
rowStates.deleting = null;
console.log('Delete modal closed without confirmation for row:', modalRowId);
}
}
if (rowStates.editing) {
// Remove the editing class from the row
const editingRow = clientTable.table.querySelector(`tbody tr[data-id="${rowStates.editing}"]`);
if (editingRow) {
editingRow.classList.remove('editing');
}
rowStates.editing = null;
}
// Reapply saved classes if any
rowStates.saved.forEach(savedRowId => {
const savedRow = clientTable.table.querySelector(`tbody tr[data-id="${savedRowId}"]`);
if (savedRow) {
savedRow.classList.add('saved');
}
});
// Also handle the adding state
if (rowStates.adding) {
rowStates.adding = false;
}
});
});