UNPKG

create-nexaui-electron

Version:

Create Nexa App - Tool untuk membuat aplikasi Nexa Electron

1,630 lines (1,382 loc) 58.7 kB
import NexaFilter from "./NexaFilter.js"; /** * @class View * @description Kelas untuk menangani view dan template */ export class NexaDom { /** * Membuat element template * @private */ createTemplateElement(firstKey, elementById, oldElement) { const template = document.createElement("script"); template.type = "text/template"; template.setAttribute("data-template", "list"); template.id = `${firstKey}_${elementById}_${this._instanceId}`; template.setAttribute("data-nexadom", this._instanceId); let templateHtml = oldElement.innerHTML; templateHtml = templateHtml.trim(); template.innerHTML = templateHtml; oldElement.parentNode.replaceChild(template, oldElement); return template; } /** * Membuat element konten * @private */ createContentElement(elementById, className) { const content = document.createElement("div"); content.id = `${elementById}_content_${this._instanceId}`; content.setAttribute("data-nexadom", this._instanceId); if (className) content.className = className; const template = document.querySelector( `[data-nexadom="${this._instanceId}"]` ); if (template) { template.parentNode.insertBefore(content, template.nextSibling); } return content; } constructor(options) { // Tambahkan unique identifier this._instanceId = `nexadom_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; // Gunakan WeakMap untuk data private this._storage = new WeakMap(); this._storage.set(this, { data: null, filter: new NexaFilter(), domElements: {}, templateCache: new Map(), fragmentCache: new Map(), }); if (!options || typeof options !== "object") { throw new Error("Parameter options harus berupa object"); } // Modifikasi element ID untuk mencegah konflik const elementId = `${options.elementById}_${this._instanceId}`; const oldElement = document.getElementById(options.elementById); if (!oldElement) { throw new Error( `Element dengan ID ${options.elementById} tidak ditemukan` ); } // Set unique attribute untuk mengidentifikasi elements milik instance ini oldElement.setAttribute("data-nexadom", this._instanceId); // Ambil extractor dari attribute const extractor = oldElement.getAttribute("extractor") || "items"; // Modifikasi fungsi validateAndSanitizeData const validateAndSanitizeData = (data) => { // Jika data adalah options object tanpa data if (data && !data.data && !data[extractor]) { return { [extractor]: [], }; } // Jika data adalah array if (Array.isArray(data)) { return { [extractor]: data, }; } // Jika data adalah object dengan property data if (data && data.data) { return { [extractor]: Array.isArray(data.data) ? data.data : [], }; } // Jika data adalah object dengan extractor key if (data && data[extractor]) { return { [extractor]: Array.isArray(data[extractor]) ? data[extractor] : [], }; } // Default return empty array return { [extractor]: [], }; }; // Deklarasikan data dan originalData sebagai property class this._data = validateAndSanitizeData(options); this._originalData = { ...this._data }; // Terapkan pengurutan awal jika sortOrder didefinisikan if (options.sortOrder && options.sortBy) { const order = options.sortOrder.toUpperCase(); if (this._data[extractor] && this._data[extractor].length > 0) { this._data[extractor] = sortData( this._data[extractor], order, options.sortBy ); this._originalData[extractor] = [...this._data[extractor]]; } } // Gunakan getter/setter untuk data Object.defineProperty(this, "data", { get: () => this._data, set: (value) => { this._data = validateAndSanitizeData(value); this._originalData = { ...this._data }; }, }); // Update storage const storage = this._storage.get(this); storage.data = this._data; // Gunakan extractor untuk template const firstKey = extractor; const sID = "[@" + firstKey + "]"; const eID = "[/" + firstKey + "]"; const rowID = firstKey; // Inisialisasi NexaDomextractor dengan passing filter const domManager = new NexaDomextractor(this.filter); // Buat template element sekali saja const templateElement = this.createTemplateElement( firstKey, options.elementById, oldElement ); const contentElement = this.createContentElement( options.elementById, oldElement.className ); // Setup template - HAPUS sID dan eID const template = templateElement.innerHTML; // Hapus sID dan eID // Cache DOM elements dan event listeners untuk cleanup const domElements = { template: templateElement, content: contentElement, searchInput: options.search ? document.getElementById(options.search) : null, }; // Tambahkan instance NexaFilter this.filter = new NexaFilter(); this._templateSelector = options.templateSelector || '[data-template="list"]'; // Implementasi deep copy yang aman function deepCopy(obj) { if (obj === null || typeof obj !== "object") return obj; const copy = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = deepCopy(obj[key]); } } return copy; } // Fungsi untuk mengurutkan data function sortData(data, order = "ASC", sortBy = "id") { if (!Array.isArray(data)) return data; return [...data].sort((a, b) => { const valueA = a[sortBy]; const valueB = b[sortBy]; // Handle nilai null atau undefined if (valueA === null || valueA === undefined) return order === "ASC" ? -1 : 1; if (valueB === null || valueB === undefined) return order === "ASC" ? 1 : -1; // Handle tipe data berbeda if (typeof valueA === "string" && typeof valueB === "string") { return order === "ASC" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); } // Handle angka dan tipe data lainnya return order === "ASC" ? valueA > valueB ? 1 : -1 : valueA < valueB ? 1 : -1; }); } /** * @param {string} str - String yang akan dikonversi menjadi slug * @returns {string} Slug yang dihasilkan */ function createSlug(str) { return str .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-"); } /** * Memproses data dengan menambahkan slug */ function processDataWithSlug(data) { if (!Array.isArray(data)) return data; return data.map((item) => ({ ...item, slug: item.href ? createSlug(item.href) : null, })); } const pageLimit = options.order || 10; // Inisialisasi data dengan slug if (this._data[rowID]) { this._data[rowID] = processDataWithSlug(this._data[rowID]); } // Pindahkan deklarasi currentPage dan totalPages ke atas sebelum digunakan let currentPage = 1; const totalPages = Math.ceil(this._data[rowID].length / pageLimit); // Inisialisasi self untuk digunakan dalam function const self = this; // Inisialisasi _activeFilters this._activeFilters = {}; // Definisikan getFilteredData di awal, sebelum digunakan function getFilteredData() { // Jika ada filter aktif, gunakan data yang sudah difilter if (Object.keys(self._activeFilters).length > 0) { return self._data[rowID].filter((item) => { return Object.entries(self._activeFilters).every( ([filterType, filterValue]) => { return String(item[filterType]) === String(filterValue); } ); }); } // Jika tidak ada filter, kembalikan semua data return self._data[rowID]; } // Tambahkan fungsi curPage yang menggunakan getFilteredData function curPage(page = 1) { const filteredData = getFilteredData(); const startIndex = (page - 1) * pageLimit; const slicedData = filteredData.slice(startIndex, startIndex + pageLimit); return { [rowID]: slicedData }; } // Cache untuk template yang sudah dirender const templateCache = new Map(); // Cache untuk fragment DOM const fragmentCache = new Map(); /** * Optimasi render dengan caching */ function optimizedRender(data, templateId) { const cacheKey = JSON.stringify(data) + templateId; // Cek cache if (templateCache.has(cacheKey)) { return templateCache.get(cacheKey); } // Render template menggunakan NexaDomextractor const rendered = domManager.render(template, data, templateElement); // Simpan ke cache dengan batasan ukuran if (templateCache.size > 100) { const firstKey = templateCache.keys().next().value; templateCache.delete(firstKey); } templateCache.set(cacheKey, rendered); return rendered; } /** * Fragment caching untuk performa */ function createCachedFragment(items, templateId) { const cacheKey = templateId + items.length; if (fragmentCache.has(cacheKey)) { return fragmentCache.get(cacheKey).cloneNode(true); } const fragment = document.createDocumentFragment(); items.forEach((item) => { const rendered = optimizedRender({ [rowID]: [item] }, templateId); const div = document.createElement("div"); div.innerHTML = rendered; fragment.appendChild(div.firstChild); }); fragmentCache.set(cacheKey, fragment.cloneNode(true)); return fragment; } /** * @param {Object} pageData - Data yang akan dirender */ function renderData(pageData) { requestAnimationFrame(() => { try { // Set pagination info sebelum render domManager.setPaginationInfo(currentPage, pageLimit); // Clear content terlebih dahulu contentElement.innerHTML = ""; // Render data const rendered = domManager.render( template, pageData, templateElement ); contentElement.innerHTML = rendered; // Update pagination setelah render updatePaginationUI(); } catch (error) { console.error("Error saat render data:", error); } }); } // Tambahkan fungsi untuk virtual scrolling jika diperlukan function setupVirtualScroll() { if (!options.virtualScroll) return; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { // Load more data when reaching bottom if (currentPage < totalPages) { currentPage++; renderData(curPage(currentPage)); } } }); }); // Observe last item const lastItem = contentElement.lastElementChild; if (lastItem) { observer.observe(lastItem); } // Cleanup return () => observer.disconnect(); } // Cache DOM queries dan kalkulasi yang sering digunakan const cachedData = { totalPages: Math.ceil(this._data[rowID].length / pageLimit), searchDebounceTimer: null, // Cache DOM elements yang sering digunakan paginationElement: options.hasOwnProperty("pagination") && options.pagination !== false ? document.getElementById(options.pagination) : null, searchInput: options.hasOwnProperty("search") && options.search !== false ? document.getElementById(options.search) : null, // Tambahkan filter select filterSelect: options.hasOwnProperty("filter") && options.filter !== false ? document.getElementById(options.filter) : null, }; /** * Fungsi debounce untuk search dengan cleanup */ function debounceSearch(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; fn.apply(this, args); }, delay); }; } // Tambahkan fungsi untuk membuat indeks pencarian function createSearchIndex(data, searchableFields) { const searchIndex = new Map(); data.forEach((item, index) => { let searchText = searchableFields .map((field) => { return item[field] ? String(item[field]).toLowerCase() : ""; }) .join(" "); searchIndex.set(index, searchText); }); return searchIndex; } // Modifikasi memoizedFilter const memoizedFilter = (function () { const cache = new Map(); let searchIndex = null; return function (keyword, data, searchableFields) { const cacheKey = keyword.trim().toLowerCase(); if (cache.has(cacheKey)) return cache.get(cacheKey); // Buat indeks jika belum ada if (!searchIndex) { searchIndex = createSearchIndex(data, searchableFields); } const filtered = data.filter((item, index) => { const indexedText = searchIndex.get(index); return indexedText.includes(cacheKey); }); cache.set(cacheKey, filtered); if (cache.size > 100) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } return filtered; }; })(); function debounceAndThrottle(fn, delay = 300, throttleDelay = 100) { let debounceTimer; let throttleTimer; let lastRun = 0; return function (...args) { // Clear existing debounce timer if (debounceTimer) clearTimeout(debounceTimer); // Throttle check const now = Date.now(); if (now - lastRun >= throttleDelay) { fn.apply(this, args); lastRun = now; } else { // Debounce debounceTimer = setTimeout(() => { fn.apply(this, args); lastRun = Date.now(); }, delay); } }; } function handleSearch(keyword, searchableFields) { const self = this; if (!keyword) { self._data[rowID] = [...self._originalData[rowID]]; } else { self._data[rowID] = self._originalData[rowID].filter((item) => { return searchableFields.some((field) => { const value = item[field]; return ( value && value.toString().toLowerCase().includes(keyword.toLowerCase()) ); }); }); } currentPage = 1; renderData(curPage(1)); updatePaginationUI(); } function setupSearch() { if (!options.search) return; const searchInput = document.getElementById(options.search); if (!searchInput) { console.warn("Search input tidak ditemukan"); return; } const searchHandler = debounceAndThrottle( (event) => { const keyword = event.target.value.trim(); handleSearch.call(self, keyword, options.searchableFields); }, 300, 100 ); searchInput.removeEventListener("input", searchHandler); searchInput.addEventListener("input", searchHandler); } // Modifikasi setupFilter function setupFilter() { if (!options.hasOwnProperty("filter")) return; const filterSelect = document.getElementById(options.filter); if (!filterSelect) return; filterSelect.addEventListener("change", function (event) { const value = event.target.value; if (value === "all") { this._data[rowID] = [...this._originalData[rowID]]; renderData(curPage(1)); return; } handleFilter(value, [options.filterBy]); }); } // Modifikasi destroy untuk cleanup worker this.destroy = function () { if (cachedData.searchInput) { cachedData.searchInput.removeEventListener("input", handleSearch); } // Cleanup untuk filter if (cachedData.filterElements) { cachedData.filterElements.forEach(({ element, handler }) => { element.removeEventListener("change", handler); }); } if (cachedData.searchDebounceTimer) { clearTimeout(cachedData.searchDebounceTimer); } // Clear memoization cache memoizedFilter.cache = new Map(); // Cleanup DOM elements domElements.content?.remove(); domElements.template?.remove(); // Clear cached data Object.keys(cachedData).forEach((key) => { cachedData[key] = null; }); // Cleanup semua elements dengan instance ID ini document .querySelectorAll(`[data-nexadom="${this._instanceId}"]`) .forEach((el) => el.remove()); // Clear storage const storage = this._storage.get(this); if (storage) { storage.templateCache.clear(); storage.fragmentCache.clear(); storage.domElements = {}; this._storage.delete(this); } }; /** * Setup filter select untuk multiple filter */ const setupFilterSelect = () => { if (!options.hasOwnProperty("filterBy")) { return false; } // Support untuk multiple filter const filterBy = Array.isArray(options.filterBy) ? options.filterBy : [options.filterBy]; // Gunakan this langsung karena arrow function const handleFilter = (event) => { const selectedValue = event.target.value; const filterType = event.target.getAttribute("data-filter-type"); // Reset ke halaman pertama saat filter berubah currentPage = 1; // Update nilai filter aktif if (selectedValue === "all") { delete this._activeFilters[filterType]; } else { this._activeFilters[filterType] = selectedValue; } requestAnimationFrame(() => { // Reset data terlebih dahulu this._data[rowID] = [...this._originalData[rowID]]; // Filter data berdasarkan semua filter yang aktif if (Object.keys(this._activeFilters).length > 0) { this._data[rowID] = this._data[rowID].filter((item) => { return Object.entries(this._activeFilters).every( ([filterType, filterValue]) => { if (!item.hasOwnProperty(filterType)) { console.warn( `Properti "${filterType}" tidak ditemukan pada item:`, item ); return false; } return String(item[filterType]) === String(filterValue); } ); }); } // Update total pages berdasarkan data yang sudah difilter const totalItems = this._data[rowID].length; cachedData.totalPages = Math.ceil(totalItems / pageLimit); // Pastikan current page valid if (currentPage > cachedData.totalPages) { currentPage = cachedData.totalPages || 1; } // Batch DOM updates const updates = () => { // Render data halaman pertama renderData(curPage(currentPage)); // Update tampilan pagination if (cachedData.paginationElement) { updatePaginationUI(); } }; requestAnimationFrame(updates); }); }; // Setup event listeners untuk setiap filter select filterBy.forEach((filterType) => { const selectElement = document.getElementById(filterType); if (selectElement) { selectElement.setAttribute("data-filter-type", filterType); // Cleanup dan setup event listener selectElement.removeEventListener("change", handleFilter); selectElement.addEventListener("change", handleFilter); if (!cachedData.filterElements) { cachedData.filterElements = []; } cachedData.filterElements.push({ element: selectElement, handler: handleFilter, type: filterType, }); } else { console.warn( `Element filter dengan ID "${filterType}" tidak ditemukan` ); } }); return true; }; // Setup fitur-fitur dasar setupSearch(); setupFilter(); setupLazyLoading(); // Panggil setupFilterSelect setelah didefinisikan setupFilterSelect(); // Initial render renderData(curPage(1)); /** * @param {Function} callback - Callback untuk memproses element */ NexaDom.prototype.Element = function (callback) { if (typeof callback !== "function") { throw new Error("Parameter callback harus berupa function"); } const filteredData = [...this.data.data[Object.keys(this.data.data)[0]]]; callback(filteredData); }; /** * Membuat element pagination jika belum ada */ function createPaginationElement() { // Cek apakah pagination didefinisikan dan tidak false if ( !options.hasOwnProperty("pagination") || options.pagination === false ) { return null; } const paginationID = options.pagination; // Cek apakah element dengan ID yang sesuai ada di HTML let paginationElement = document.getElementById(paginationID); if (!paginationID || !paginationElement) { console.warn("Element pagination tidak ditemukan atau ID tidak sesuai"); return null; } // Pastikan element memiliki class pagination if (!paginationElement.classList.contains("pagination")) { paginationElement.classList.add("pagination"); } return paginationElement; } /** * Membuat dan memperbarui UI pagination */ function updatePaginationUI() { // Cek apakah pagination didefinisikan dan tidak false if ( !options.hasOwnProperty("pagination") || options.pagination === false ) { return false; } const paginationList = createPaginationElement(); if (!paginationList) return false; // Reset pagination content paginationList.innerHTML = ""; // Gunakan getFilteredData yang sudah didefinisikan di atas const filteredData = getFilteredData(); const totalItems = filteredData.length; const currentTotalPages = Math.ceil(totalItems / pageLimit); // Validasi current page if (currentPage > currentTotalPages) { currentPage = currentTotalPages || 1; } // Update info halaman dan total data const pageInfoElement = document.getElementById("pageInfo"); if (pageInfoElement) { const currentPageSpan = document.getElementById("currentPage"); const totalPagesSpan = document.getElementById("totalPages"); const totalItemsSpan = document.getElementById("totalItems"); if (currentPageSpan) currentPageSpan.textContent = currentPage; if (totalPagesSpan) totalPagesSpan.textContent = currentTotalPages; if (totalItemsSpan) totalItemsSpan.textContent = totalItems; } // Jika tidak ada data atau hanya 1 halaman, sembunyikan pagination if (currentTotalPages <= 1) { paginationList.style.display = "none"; return false; } paginationList.style.display = "flex"; // Tombol First const firstLi = document.createElement("li"); firstLi.classList.add("page-item"); if (currentPage === 1) firstLi.classList.add("disabled"); firstLi.innerHTML = `<button class="page-link" data-page="1">First</button>`; paginationList.appendChild(firstLi); // Tombol Previous const prevLi = document.createElement("li"); prevLi.classList.add("page-item"); if (currentPage === 1) prevLi.classList.add("disabled"); prevLi.innerHTML = `<button class="page-link" data-page="${ currentPage - 1 }">Previous</button>`; paginationList.appendChild(prevLi); // Logika untuk menampilkan nomor halaman let startPage, endPage; if (currentTotalPages <= 5) { startPage = 1; endPage = currentTotalPages; } else { if (currentPage <= 3) { startPage = 1; endPage = 5; } else if (currentPage >= currentTotalPages - 2) { startPage = currentTotalPages - 4; endPage = currentTotalPages; } else { startPage = currentPage - 2; endPage = currentPage + 2; } } // Render nomor halaman for (let i = startPage; i <= endPage; i++) { const pageLi = document.createElement("li"); pageLi.classList.add("page-item"); if (i === currentPage) pageLi.classList.add("active"); pageLi.innerHTML = `<button class="page-link" data-page="${i}">${i}</button>`; paginationList.appendChild(pageLi); } // Tombol Next const nextLi = document.createElement("li"); nextLi.classList.add("page-item"); if (currentPage === currentTotalPages) nextLi.classList.add("disabled"); nextLi.innerHTML = `<button class="page-link" data-page="${ currentPage + 1 }">Next</button>`; paginationList.appendChild(nextLi); // Tombol Last const lastLi = document.createElement("li"); lastLi.classList.add("page-item"); if (currentPage === currentTotalPages) lastLi.classList.add("disabled"); lastLi.innerHTML = `<button class="page-link" data-page="${currentTotalPages}">Last</button>`; paginationList.appendChild(lastLi); // Tambahkan event listener untuk pagination buttons paginationList.querySelectorAll(".page-link").forEach((button) => { button.addEventListener("click", function () { const newPage = parseInt(this.dataset.page); if ( newPage && newPage !== currentPage && newPage >= 1 && newPage <= currentTotalPages ) { currentPage = newPage; renderData(curPage(currentPage)); updatePaginationUI(); } }); }); } /** * Setup event listeners untuk pagination */ function setupPaginationListeners() { const paginationList = document.getElementById(options.pagination); if (!paginationList) return; paginationList.addEventListener("click", function (event) { const button = event.target.closest(".page-link"); if (!button) return; const newPage = parseInt(button.dataset.page); if (isNaN(newPage) || newPage < 1 || newPage > totalPages) return; if (newPage === currentPage) return; currentPage = newPage; renderData(curPage(currentPage)); updatePaginationUI(); // Update info page setiap kali halaman berubah const pageInfo = { pagination: { currentPage: currentPage, totalPages: Math.ceil(this._data[rowID].length / pageLimit), totalItems: this._data[rowID].length, }, }; const currentPageSpan = document.getElementById("currentPage"); if (currentPageSpan) { currentPageSpan.textContent = pageInfo.pagination.currentPage; } }); } // Initial setup setupPaginationListeners(); /** * Fungsi untuk memuat ulang data * @param {Object|Array} newData - Data baru yang akan dimuat * @param {boolean} resetPage - Reset ke halaman pertama (default: true) */ NexaDom.prototype.addData = function (newData, resetPage = true) { if ( !newData || (typeof newData !== "object" && !Array.isArray(newData)) ) { throw new Error("Parameter newData harus berupa object atau array"); } try { // Backup data lama untuk rollback jika terjadi error const oldData = { ...this._data }; const oldOriginalData = { ...this._originalData }; let newItems = []; // Validasi dan ekstrak data baru if (Array.isArray(newData)) { newItems = [...newData]; } else { const firstKey = Object.keys(newData)[0]; if (!firstKey || !Array.isArray(newData[firstKey])) { throw new Error("Data harus berupa array atau object dengan array"); } newItems = [...newData[firstKey]]; } // Validasi data baru tidak kosong if (newItems.length === 0) { throw new Error("Data baru tidak boleh kosong"); } try { // Proses data baru dengan slug const processedNewItems = processDataWithSlug(newItems); // Gabungkan data baru di awal dengan data lama this._originalData[rowID] = [ ...newItems, ...this._originalData[rowID], ]; this._data[rowID] = [...processedNewItems, ...this._data[rowID]]; } catch (slugError) { console.warn("Error saat memproses slug:", slugError); // Jika gagal memproses slug, tetap gabungkan data tanpa slug this._originalData[rowID] = [ ...newItems, ...this._originalData[rowID], ]; this._data[rowID] = [...newItems, ...this._data[rowID]]; } // Update cache dan perhitungan terkait cachedData.totalPages = Math.ceil(this._data[rowID].length / pageLimit); // Reset pencarian jika ada if (cachedData.searchInput) { cachedData.searchInput.value = ""; } // Reset ke halaman pertama jika diminta if (resetPage) { currentPage = 1; } // Clear memoization cache karena data berubah if (memoizedFilter && memoizedFilter.cache) { memoizedFilter.cache.clear(); } // Render ulang dengan data baru requestAnimationFrame(() => { renderData(curPage(currentPage)); // Trigger custom event untuk notifikasi reload selesai const reloadEvent = new CustomEvent("dataReloaded", { detail: { success: true, newItemsCount: newItems.length, totalItems: this._data[rowID].length, currentPage: currentPage, }, }); document.dispatchEvent(reloadEvent); }); return true; } catch (error) { console.error("Error saat reload data:", error); // Rollback ke data lama jika terjadi error this._data = { ...oldData }; this._originalData = { ...oldOriginalData }; // Trigger custom event untuk notifikasi error const errorEvent = new CustomEvent("dataReloadError", { detail: { error: error.message, }, }); document.dispatchEvent(errorEvent); return false; } }; /** * Fungsi untuk mendapatkan data saat ini * @returns {Object} Data yang sedang ditampilkan */ NexaDom.prototype.getCurrentData = function () { const totalItems = this._data[rowID].length; const currentTotalPages = Math.ceil(totalItems / pageLimit); return { all: this._data[rowID], current: this.curPage(currentPage)[rowID], pagination: { currentPage: currentPage, totalPages: currentTotalPages, pageLimit: pageLimit, totalItems: totalItems, }, }; }; /** * Fungsi untuk refresh manual dengan tombol * @param {string} buttonId - ID tombol refresh * @param {Object} options - Data dan opsi refresh */ NexaDom.prototype.setupRefreshButton = function (buttonId, options = {}) { const refreshButton = document.getElementById(buttonId); if (!refreshButton) { console.warn("Tombol refresh tidak ditemukan:", buttonId); return null; } const { onStart, onSuccess, onError, loadingText = "Memperbarui...", data = null, reloadOptions = {}, } = options; let isLoading = false; const originalText = refreshButton.innerHTML; const handleRefresh = async () => { if (isLoading) return; try { isLoading = true; refreshButton.disabled = true; refreshButton.innerHTML = loadingText; if (onStart) await onStart(); if (data) { const success = this.Reload(data, reloadOptions); if (success && onSuccess) { await onSuccess({ data }); } } } catch (error) { console.error("Error saat refresh:", error); if (onError) await onError(error); } finally { isLoading = false; refreshButton.disabled = false; refreshButton.innerHTML = originalText; } }; const cleanup = () => { refreshButton.removeEventListener("click", handleRefresh); }; refreshButton.addEventListener("click", handleRefresh); return cleanup; }; /** * Fungsi untuk refresh dengan onclick * @param {Object} data - Data untuk refresh */ NexaDom.prototype.Reload = function (newData, options = {}) { try { // Validasi data dengan lebih fleksibel let dataToLoad; const rowID = this._rowID; // Gunakan _rowID yang sudah diinisialisasi if (Array.isArray(newData)) { dataToLoad = { [rowID]: newData }; } else if (newData && newData.data && newData.data[rowID]) { dataToLoad = { [rowID]: newData.data[rowID] }; } else if (newData && newData[rowID]) { dataToLoad = newData; } else { throw new Error("Format data tidak valid untuk reload"); } const { append = false, preserveFilters = false, resetPage = true, } = options; // Backup data lama untuk rollback const oldData = [...this._data[rowID]]; const oldOriginalData = [...this._originalData[rowID]]; try { if (append) { // Tambahkan data baru ke existing data this._originalData[rowID] = [ ...this._originalData[rowID], ...dataToLoad[rowID], ]; this._data[rowID] = [...this._data[rowID], ...dataToLoad[rowID]]; } else { // Ganti dengan data baru this._originalData[rowID] = [...dataToLoad[rowID]]; this._data[rowID] = [...dataToLoad[rowID]]; } // Reset ke halaman pertama jika diminta if (resetPage) { this._currentPage = 1; } // Update UI this.renderData(this.curPage(this._currentPage)); return { success: true, totalItems: this._data[rowID].length, currentPage: this._currentPage, }; } catch (error) { // Rollback jika terjadi error this._data[rowID] = oldData; this._originalData[rowID] = oldOriginalData; throw error; } } catch (error) { console.error("Error dalam Reload:", error); return { success: false, error: error.message, }; } }; function renderLargeTemplate(template, data) { const chunkSize = 1000; // karakter const chunks = []; for (let i = 0; i < template.length; i += chunkSize) { chunks.push(template.slice(i, i + chunkSize)); } let result = ""; chunks.forEach((chunk, index) => { requestAnimationFrame(() => { result += processTemplateChunk(chunk, data); if (index === chunks.length - 1) { contentElement.innerHTML = result; } }); }); } /** * Setup lazy loading untuk gambar dan konten */ function setupLazyLoading() { const options = { root: null, rootMargin: "50px", threshold: 0.1, }; // Observer untuk gambar const imageObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { // Load gambar dengan fade effect img.style.opacity = "0"; img.src = img.dataset.src; img.onload = () => { img.style.transition = "opacity 0.3s"; img.style.opacity = "1"; }; delete img.dataset.src; imageObserver.unobserve(img); } } }); }, options); // Observer untuk konten berat const contentObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const element = entry.target; if (element.dataset.content) { loadHeavyContent(element); contentObserver.unobserve(element); } } }); }, options); // Load konten berat function loadHeavyContent(element) { const contentId = element.dataset.content; } // Observe semua gambar lazy document.querySelectorAll("img[data-src]").forEach((img) => { imageObserver.observe(img); }); // Observe konten berat document.querySelectorAll("[data-content]").forEach((element) => { contentObserver.observe(element); }); return { imageObserver, contentObserver, }; } // Cleanup untuk lazy loading this.destroy = function () { if (this.lazyLoadObservers) { this.lazyLoadObservers.imageObserver.disconnect(); this.lazyLoadObservers.contentObserver.disconnect(); } // ... cleanup lainnya }; // Setup lazy loading saat inisialisasi this.lazyLoadObservers = setupLazyLoading(); /** * Setup virtual scrolling untuk data besar */ function setupVirtualScrolling() { const viewportHeight = window.innerHeight; const itemHeight = 50; // Perkiraan tinggi setiap item const bufferSize = 5; // Jumlah item buffer atas dan bawah const visibleItems = Math.ceil(viewportHeight / itemHeight) + bufferSize * 2; let startIndex = 0; let scrollTimeout; const container = contentElement; const scrollContainer = document.createElement("div"); scrollContainer.style.position = "relative"; container.appendChild(scrollContainer); function updateVisibleItems() { const scrollTop = container.scrollTop; startIndex = Math.floor(scrollTop / itemHeight); startIndex = Math.max(0, startIndex - bufferSize); const visibleData = this._data[rowID].slice( startIndex, startIndex + visibleItems ); const totalHeight = this._data[rowID].length * itemHeight; scrollContainer.style.height = `${totalHeight}px`; // Render hanya item yang visible const fragment = document.createDocumentFragment(); visibleData.forEach((item, index) => { const itemElement = document.createElement("div"); itemElement.style.position = "absolute"; itemElement.style.top = `${(startIndex + index) * itemHeight}px`; itemElement.style.height = `${itemHeight}px`; const rendered = optimizedRender({ [rowID]: [item] }, rowID); itemElement.innerHTML = rendered; fragment.appendChild(itemElement); }); // Clear dan update content while (scrollContainer.firstChild) { scrollContainer.removeChild(scrollContainer.firstChild); } scrollContainer.appendChild(fragment); } container.addEventListener("scroll", () => { if (scrollTimeout) { cancelAnimationFrame(scrollTimeout); } scrollTimeout = requestAnimationFrame(updateVisibleItems); }); // Initial render updateVisibleItems(); return { refresh: updateVisibleItems, destroy: () => { container.removeEventListener("scroll", updateVisibleItems); scrollContainer.remove(); }, }; } /** * Setup data chunking dan storage */ class DataChunkManager { constructor(dbName = "viewDB", storeName = "chunks") { this.dbName = dbName; this.storeName = storeName; this.chunkSize = 1000; // Items per chunk this.db = null; } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, { keyPath: "chunkId" }); } }; }); } async storeChunks(data) { const chunks = this.createChunks(data); const store = this.db .transaction(this.storeName, "readwrite") .objectStore(this.storeName); return Promise.all( chunks.map( (chunk) => new Promise((resolve, reject) => { const request = store.put(chunk); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }) ) ); } async getChunk(chunkId) { return new Promise((resolve, reject) => { const request = this.db .transaction(this.storeName) .objectStore(this.storeName) .get(chunkId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } createChunks(data) { const chunks = []; for (let i = 0; i < data.length; i += this.chunkSize) { chunks.push({ chunkId: Math.floor(i / this.chunkSize), data: data.slice(i, i + this.chunkSize), }); } return chunks; } } /** * Setup data streaming untuk load data besar */ class DataStreamManager { constructor(options = {}) { this.pageSize = options.pageSize || 50; this.chunkManager = new DataChunkManager(); } async init() { await this.chunkManager.init(); this.setupStreamHandlers(); } setupStreamHandlers() { let currentChunk = 0; let isLoading = false; const loadNextChunk = async () => { if (isLoading) return; isLoading = true; try { const chunk = await this.chunkManager.getChunk(currentChunk); if (chunk) { // Process chunk dengan worker this.worker.postMessage({ action: "processChunk", data: chunk.data, }); currentChunk++; } } catch (error) { console.error("Error loading chunk:", error); } finally { isLoading = false; } }; // Setup intersection observer untuk infinite scroll const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadNextChunk(); } }, { threshold: 0.5 } ); // Observe loader element const loader = document.querySelector("#chunk-loader"); if (loader) { observer.observe(loader); } } async processStreamedData(data) { // Store chunks di IndexedDB await this.chunkManager.storeChunks(data); // Setup virtual scrolling const virtualScroller = setupVirtualScrolling(); return { destroy: () => { virtualScroller.destroy(); // Cleanup lainnya }, }; } } // Inisialisasi managers const streamManager = new DataStreamManager(); let virtualScroller = null; // Bind this ke initializeDataHandling const initializeDataHandling = async () => { await streamManager.init(); if (this._data[rowID].length > 1000) { // Gunakan virtual scrolling untuk data besar virtualScroller = setupVirtualScrolling(); // Process data dengan streaming await streamManager.processStreamedData(this._data[rowID]); } else { // Render normal untuk data kecil renderData(curPage(1)); } }; // Update destroy method to remove worker cleanup this.destroy = function () { if (virtualScroller) { virtualScroller.destroy(); } // ... other cleanup }; // Initialize dengan arrow function untuk mempertahankan this initializeDataHandling().catch(console.error); /** * Filter data berdasarkan key dan value */ this.filterKey = function (key, value) { if (!key || !value) { console.warn("Parameter key dan value harus diisi"); return this; } try { // Filter data berdasarkan key dan value this._data[rowID] = this._originalData[rowID].filter((item) => { return String(item[key]) === String(value); }); // Update UI currentPage = 1; renderData(curPage(1)); updatePaginationUI(); // Return object dengan informasi hasil filter return { filtered: this._data[rowID].length, total: this._originalData[rowID].length, data: this._data[rowID], }; } catch (error) { console.error("Error dalam filterKey:", error); return { filtered: 0, total: this._originalData[rowID].length, data: [], }; } }; /** * Internal filter state management */ this._filterState = { active: {}, history: [], add: function (key, value) { this.active[key] = value; this.history.push({ key, value, timestamp: Date.now(), }); }, remove: function (key) { delete this.active[key]; }, clear: function () { this.active = {}; this.history = []; }, get: function (key) { return this.active[key]; }, getAll: function () { return { ...this.active }; }, }; /** * Internal filter helper */ this._internalFilter = function (key, value) { if (!key || !value) return; try { this._data[rowID] = this._data[rowID].filter((item) => { // Handle nested object if (key.includes(".")) { const keys = key.split("."); let val = item; for (const k of keys) { if (val === undefined) return false; val = val[k]; } return String(val) === String(value); } // Handle array value if (Array.isArray(value)) { return value.includes(String(item[key])); } return String(item[key]) === String(value); }); } catch (error) { console.error