_pageDriverVersion = '1.0.1'; // Handle color for icon svg with id="logo" based on the current theme const logo = document.getElementById('logo'); if (logo) { logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue('--bulma-text'); } if (_wrapperVersion === undefined) { console.error('API Wrapper not found; Please include the API Wrapper before including the Page Driver'); exit(); } else { console.log('API Wrapper found; Page Driver is ready to use'); } // Find all tables on the page which have data-dataSource attribute var tables = document.querySelectorAll('table[data-dataSource]'); //var tables = [] // Get all single values with data-dataSource, data-dataCol and data-dataAction var singleValues = document.querySelectorAll('span[data-dataSource]'); // Find all search fields with data-searchTargetId var searchFields = document.querySelectorAll('input[data-searchTargetId]'); // Find all modalForms var modalForms = document.querySelectorAll('form[data-targetTable]'); console.info('Processing single values'); console.info(singleValues); // Iterate over all single values singleValues.forEach(async (singleValue) => { writeSingelton(singleValue); }); // Iterate over all tables tables.forEach(async (table) => { // Get THs and attach onClick event to sort const ths = table.querySelectorAll('th'); ths.forEach((th) => { if(th.getAttribute('fnc') == "actions") { return; } th.style.cursor = 'pointer'; th.style.userSelect = 'none'; th.addEventListener('click', async function () { const table = th.closest('table'); const order = th.getAttribute('data-order'); // Clear all other order attributes ths.forEach((th) => { if (th != this) { th.removeAttribute('data-order'); th.classList.remove("bi-caret-up-fill") th.classList.remove("bi-caret-down-fill") } }); if (order == 'ASC') { th.setAttribute('data-order', 'DESC'); th.classList.add("bi-caret-down-fill") th.classList.remove("bi-caret-up-fill") } else { th.setAttribute('data-order', 'ASC'); th.classList.add("bi-caret-up-fill") th.classList.remove("bi-caret-down-fill") } refreshTable(table); }); }); refreshTable(table); }); async function writeSingelton(element) { const table = element.getAttribute('data-dataSource'); console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element); switch (element.getAttribute('data-dataAction')) { case 'COUNT': { console.log('Count action found'); element.innerHTML = await getCountByTable(table); break; } case 'SPECIAL': { if (table == 'version') { element.innerHTML = (await getServerVersion())['version']; break; } } default: { console.error('Unknown action found: ', element.getAttribute('data-dataAction')); break; } } element.classList.remove('is-skeleton'); } // Attach listeners to search fields searchFields.forEach((searchField) => { // Apply restrictions to search field (min, max, chars, etc) getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => { desc = desc['GET']['keys']['search']; var rules = desc['rules']; rules.forEach((rule) => { switch (rule['name']) { case 'min': { searchField.setAttribute('minlength', rule['args']['limit']); break; } case 'max': { searchField.setAttribute('maxlength', rule['args']['limit']); break; } } }); }); searchField.addEventListener('input', async function () { console.log('Search field changed: ', searchField); if (searchField.checkValidity() == false) { console.log('Invalid input'); searchField.classList.add('is-danger'); return; } else { searchField.classList.remove('is-danger'); const targetId = searchField.getAttribute('data-searchTargetId'); const target = document.getElementById(targetId); const table = target.getAttribute('data-dataSource'); const column = target.getAttribute('data-dataCol'); const value = searchField.value; console.log('Searching for ', value, ' in ', table, ' column ', column); refreshTableByName(table); } }); }); // Attach listeners to modal forms modalForms.forEach((modalForm) => { // Add validation to form by using API description (everything is assumed POST for now) modalForm.addEventListener('input', async function (event) { if (event.target.checkValidity() == false) { modalForm.querySelector("input[type='submit']").setAttribute('disabled', true); event.target.classList.add('is-danger'); return; } else { modalForm.querySelector("input[type='submit']").removeAttribute('disabled'); event.target.classList.remove('is-danger'); } }); getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => { console.log('Description: ', desc); const keys = desc['POST']['keys']; // Apply resitrictions and types to form fields for (key in keys) { const field = modalForm.querySelector("input[name='" + key + "']"); if (field) { const rules = keys[key]['rules']; const flags = keys[key]['flags']; console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags); rules.forEach((rule) => { switch (rule['name']) { case 'min': { field.setAttribute('minlength', rule['args']['limit']); break; } case 'max': { field.setAttribute('maxlength', rule['args']['limit']); break; } case 'pattern': { field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1)); //field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$"); break; } case 'type': { //field.setAttribute("type", rule["args"]["type"]); console.log('Type: ', rule['args']['type']); break; } } }); if (flags) { flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required'); } } } console.log('Keys: ', keys); }); modalForm.addEventListener('submit', async function (event) { event.preventDefault(); // Check what button submitted the form and if it has data-actionBtn = save // If not, close modal const pressedBtn = event.submitter; if (pressedBtn.getAttribute('data-actionBtn') != 'save') { modalForm.closest('.modal').classList.remove('is-active'); return; } // Find .entryPhase and hide it const entryPhase = modalForm.querySelector('.entryPhase'); const loadPhase = modalForm.querySelector('.loadPhase'); if (entryPhase) { entryPhase.classList.add('is-hidden'); } if (loadPhase) { loadPhase.classList.remove('is-hidden'); } console.log('Form submitted: ', modalForm); const table = modalForm.getAttribute('data-targetTable'); const data = new FormData(modalForm); // Convert to JSON object let jsonData = {}; data.forEach((value, key) => { jsonData[key] = value; }); console.log('JSON Data: ', jsonData); let resp = {}; if(modalForm.getAttribute('data-action') == 'edit') { Rid = modalForm.getAttribute('data-rid'); resp = await updateRow(table, Rid,jsonData); modalForm.setAttribute('data-action', 'create'); } else { resp = await createEntry(table, jsonData); } console.log('Response: ', resp); if (resp['status'] == 'CREATED' || resp['status'] == 'UPDATED') { console.log('Entry created successfully'); modalForm.closest('.modal').classList.remove('is-active'); modalForm.reset(); // Hide loadPhase if (loadPhase) { loadPhase.classList.add('is-hidden'); } // Show entryPhase if (entryPhase) { entryPhase.classList.remove('is-hidden'); } } else { // Hide loadPhase if (loadPhase) { loadPhase.classList.add('is-hidden'); } // Show entryPhase if (entryPhase) { entryPhase.classList.remove('is-hidden'); } // TODO: Show error message } // Find all tables with data-searchTargetId set to table setTimeout(() => { refreshTableByName(table); updateSingeltonsByTableName(table); }, 500); }); }); // Helper async function refreshTable(table) { // Refresh a table while keeping (optionally set) search value const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']"); // Get state of order and sort const ths = table.querySelectorAll('th'); const columnIndices = []; ths.forEach((th, index) => { columnIndices[th.getAttribute('data-dataCol')] = index; }); let order = ''; let column = ''; ths.forEach((th) => { if (th.hasAttribute('data-order')) { order = th.getAttribute('data-order'); column = th.getAttribute('data-dataCol'); } }); console.log('Order: ', order, ' Column: ', column); const maxLinesPerPage = table.getAttribute('data-pageSize'); let currentPage = table.getAttribute('data-currentPage'); if(currentPage == null) { table.setAttribute('data-currentPage', 1); currentPage = 1; } const start = (currentPage - 1) * maxLinesPerPage; const end = start + maxLinesPerPage; let paginationPassOnPre = { 'start': start, 'end': end, 'currentPage': currentPage, 'maxLinesPerPage': maxLinesPerPage }; if (searchField) { const value = searchField.value; const dbTable = table.getAttribute('data-dataSource'); const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start); const totalResultCount = await getCountByTable(dbTable, value); paginationPassOnPre['dataLength'] = totalResultCount; var magMiddl = managePaginationMiddleware(result, paginationPassOnPre); var data = magMiddl[0]; var paginationPassOn = magMiddl[1]; clearTable(table); writeDataToTable(table, data, paginationPassOn); } else { const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start); const resultCount = await getCountByTable(table.getAttribute('data-dataSource')); paginationPassOnPre['dataLength'] = resultCount; var magMiddl = managePaginationMiddleware(result, paginationPassOnPre); var data = magMiddl[0]; var paginationPassOn = magMiddl[1]; clearTable(table); writeDataToTable(table, data, paginationPassOn); } } function managePaginationMiddleware(data, paginationPassOnPre) { const maxLinesPerPage = paginationPassOnPre['maxLinesPerPage']; const dataLength = paginationPassOnPre['dataLength']; const maxPages = Math.ceil(dataLength / maxLinesPerPage); paginationPassOn = paginationPassOnPre; paginationPassOn['maxPages'] = maxPages; // paginationPassOn['dataLength'] = dataLength; return [data, paginationPassOn]; } async function refreshTableByName(name) { const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']"); for (dirty of dirtyTables) { refreshTable(dirty); } } async function updateSingeltonsByTableName(name) { const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']"); for (dirty of dirtySingles) { writeSingelton(dirty); } } function clearTable(table) { const tbody = table.querySelector('tbody'); tbody.innerHTML = ''; } function writeDataToTable(table, data, paginationPassOn) { if(data == undefined || data == null || data.length == 0) { return; } console.log('Writing data to table: ', table, data); // Get THEAD and TBODY elements const thead = table.querySelector('thead'); const tbody = table.querySelector('tbody'); // get index per column const columns = thead.querySelectorAll('th'); const columnIndices = []; columns.forEach((column, index) => { columnIndices[column.getAttribute('data-dataCol')] = index; }); // All required cols let requiredCols = []; let actionFields = []; columns.forEach((column) => { // console.log('Column: ', column, ' FNC: ', column.getAttribute('data-fnc'), column.attributes); if(column.getAttribute('data-fnc') == "actions") { console.log('!!! Found actions column !!!'); actionFields.push(column); return; } requiredCols.push(column.getAttribute('data-dataCol')); }); // Get paginationPassOn const start = paginationPassOn['start']; const end = paginationPassOn['end']; const currentPage = paginationPassOn['currentPage']; const maxLinesPerPage = paginationPassOn['maxLinesPerPage']; const maxPages = paginationPassOn['maxPages']; const dataLength = paginationPassOn['dataLength']; // Find nav with class pagination and data-targetTable="table.id" const paginationElement = document.querySelector("nav.pagination[data-targetTable='" + table.id + "']"); const paginationList = paginationElement.querySelector('ul.pagination-list'); console.log('Data length: ', dataLength, ' Max pages: ', maxPages); if(maxPages > 1) { // Clear pagination list paginationList.innerHTML = ''; for (let i = 1; i <= maxPages; i++) { const li = document.createElement('li'); li.innerHTML = '' + i + ''; if(i == currentPage) { li.querySelector('a').classList.add('is-current'); } paginationList.appendChild(li); } // Remove unused pages, only leave first, last, current and 2 neighbors let pages = paginationList.querySelectorAll('li'); let friends = [] // Always add first and last friends.push(0); friends.push(pages.length - 1); friends.push(currentPage-1); // Add direct neighbors // friends.push(currentPage - 2); friends.push(currentPage); friends.push(currentPage - 2); // Deduplicate friends friends = [...new Set(friends)]; // Sort friends friends.sort((a, b) => a - b); // Parse friends (string to int) friends = friends.map((x) => parseInt(x)); console.log('Friends: ', friends, ' Pages: ', pages.length, ' Current: ', currentPage); // Remove everyone who is not a friend for(let i = 0; i < pages.length; i++) { if(friends.includes(i)) { continue; } pages[i].remove(); } // Find all gaps (step size bigger then 1) and add an ellipsis in between the two numbers let last = 0; for(let i = 0; i < friends.length; i++) { if(friends[i] - last > 1) { const li = document.createElement('li'); li.innerHTML = ''; paginationList.insertBefore(li, pages[friends[i]]); } last = friends[i]; } // Append on click event to all pagination links paginationList.querySelectorAll('a').forEach((link) => { link.addEventListener('click', async function() { const page = link.getAttribute('data-page'); table.setAttribute('data-currentPage', page); refreshTable(table); }); }); paginationElement.classList.remove('is-hidden'); } else { paginationElement.classList.add('is-hidden'); } for (resultIndex in data) { const row = data[resultIndex]; const tr = document.createElement('tr'); requiredCols.forEach((column) => { const td = document.createElement('td'); td.innerText = row[column]; tr.appendChild(td); }); // Add action fields actionFields.forEach((actionField) => { const td = document.createElement('td'); const actions = actionField.getAttribute('data-actions').split(','); actions.forEach((action) => { const button = document.createElement('button'); let icon = ''; let color = 'is-primary'; switch(action) { case 'edit': { icon = ''; break; } case 'delete': { icon = ''; color = 'is-danger'; break; } } // Add classes button.classList.add('button'); button.classList.add('is-small'); button.classList.add(color); button.classList.add('is-outlined'); button.innerHTML = ` ${icon} `; button.style.marginRight = '5px'; // Add data-action and data-id button.setAttribute('data-action', action); button.setAttribute("data-id", row["id"]); // Add event listener button.addEventListener('click', async function() { const table = actionField.closest('table'); const row = button.closest('tr'); const columns = table.querySelectorAll('th'); const columnIndices = []; columns.forEach((column, index) => { columnIndices[column.getAttribute('data-dataCol')] = index; }); const data = []; columns.forEach((column) => { data[column.getAttribute('data-dataCol')] = row.children[columnIndices[column.getAttribute('data-dataCol')]].innerText; }); console.log('Data: ', data); switch(action) { case 'edit': { // Open modal with form const form = document.querySelector("form[data-targetTable='" + table.getAttribute('data-dataSource') + "']"); const formTitle = form.querySelector('.title'); const entryPhase = form.querySelector('.entryPhase'); const loadPhase = form.querySelector('.loadPhase'); const fields = form.querySelectorAll('input'); // Set modal to edit mode form.setAttribute('data-action', 'edit'); form.setAttribute('data-rid', button.getAttribute('data-id')); formTitle.innerText = 'Edit entry'; fields.forEach((field) => { // Skip for submit button if(field.getAttribute('type') == 'submit') { return; } field.value = data[field.getAttribute('name')]; }); form.closest('.modal').classList.add('is-active'); // TBD break; } case 'delete': { // confirm const confirm = window.confirm('Do you really want to delete this entry?'); // Delete entry if(confirm) { const table = actionField.closest('table'); const id = button.getAttribute('data-id'); const resp = await deleteRow(table.getAttribute('data-dataSource'), id); if(resp['status'] == 'DELETED') { refreshTable(table); updateSingeltonsByTableName(table.getAttribute('data-dataSource')); } else { // Show error message // TODO: Show error message } } break; } } } ); td.appendChild(button); }); tr.appendChild(td); }); tbody.appendChild(tr); } } // Handle modal document.addEventListener('DOMContentLoaded', () => { // Functions to open and close a modal function openModal($el) { $el.classList.add('is-active'); } function closeModal($el) { $el.classList.remove('is-active'); } function closeAllModals() { (document.querySelectorAll('.modal') || []).forEach(($modal) => { closeModal($modal); }); } // Add a click event on buttons to open a specific modal (document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { const modal = $trigger.dataset.target; const $target = document.getElementById(modal); $trigger.addEventListener('click', () => { openModal($target); }); }); // Add a click event on various child elements to close the parent modal (document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { const $target = $close.closest('.modal'); $close.addEventListener('click', () => { closeModal($target); }); }); // Add a keyboard event to close all modals document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeAllModals(); } }); });