create-nexaui-electron
Version:
Create Nexa App - Tool untuk membuat aplikasi Nexa Electron
4,552 lines • 139 kB
JavaScript
// import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.12.1/firebase-app.js';
// import { getDatabase, ref, set, get, update, remove, push, onValue } from 'https://www.gstatic.com/firebasejs/9.12.1/firebase-database.js';
export function Ngorei() {
return {
Helper: function () {
return {
Encode: function (row) {
return Encode(row);
},
Decode: function (row) {
return Decode(row);
},
};
},
Tokenize: function (userData, cradensial) {
const SECRET_KEY = cradensial.replace(/-/g, "");
return tokenize(userData, SECRET_KEY);
},
Components: function () {
return {
stylesheet: function (sty) {
return stylesheet(sty);
},
UILogger: function (row) {
const NexaUI = new Ngorei().Network();
if (row === true) {
NexaUI.localStorage().add({
key: "UILogger",
data: {
status: true,
},
});
} else if (row === false) {
NexaUI.localStorage().del("UILogger");
}
// return new UILogger();
},
ViewStack: function () {
return new ViewStack();
},
SectionId: function () {
// return new SectionId();
},
Accordion: function () {
// return Accordion();
},
Alerts: function () {
// return Alerts();
},
buttonGroups: function () {
// return buttonGroups();
},
Carousel: function () {
// return initCarousels();
},
Collapse: function () {
// return Collapse();
},
Dropdown: function () {
// return Dropdown();
},
Lightbox: function () {
// return new Lightbox();
},
Sidebar: function (darkModeButton) {
// return new SidebarMenu();
},
ListGroup: function () {
// return new ListGroup();
},
Offcanvas: function () {
// return new Offcanvas();
},
Popover: function () {
// return new Popover();
},
Progress: function () {
// return Progress();
},
Scrollspy: function () {
// return Scrollspy();
},
Search: function () {
// return initializeSearch();
},
Sortable: function () {
// return new Sortable();
},
Toast: function () {
// return Toast();
},
Tooltips: function () {
// return Tooltips();
},
DarkMode: function (darkModeButton) {
//return new DarkMode();
},
getViewStack: function () {
return new getViewStack();
},
PrismTab: function () {
const tabCodeContainers = document.querySelectorAll(".nx-tabcode");
tabCodeContainers.forEach((container) => {
const tabBtns = container.querySelectorAll(".nx-tabcode-btn");
tabBtns.forEach((btn) => {
btn.addEventListener("click", function () {
// Hapus kelas active hanya dari tab dalam container yang sama
container
.querySelectorAll(".nx-tabcode-btn")
.forEach((b) => b.classList.remove("active"));
container
.querySelectorAll(".nx-tabcode-pane")
.forEach((pane) => pane.classList.remove("active"));
// Tambahkan kelas active ke tab yang diklik
this.classList.add("active");
document
.getElementById(this.dataset.tab)
.classList.add("active");
});
});
});
},
Prism: function () {
wrapCodeWithTerminal();
window.copyCode = function (button) {
try {
// Ambil kode dari elemen
const pre = button.closest(".terminal").querySelector("pre");
const code = pre.querySelector("code").innerText;
// Buat elemen textarea temporary
const textarea = document.createElement("textarea");
textarea.value = code;
textarea.style.position = "fixed"; // Hindari scrolling
textarea.style.opacity = "0"; // Sembunyikan elemen
// Tambahkan ke dokumen
document.body.appendChild(textarea);
// Select dan copy
textarea.select();
document.execCommand("copy");
// Bersihkan
document.body.removeChild(textarea);
// Feedback visual
const originalText = button.textContent;
button.innerHTML =
'<i class="icon-feather-copy"></i> ' + "Tersalin";
button.classList.add("bg-green-600");
setTimeout(() => {
button.innerHTML =
'<i class="icon-feather-copy"></i> ' + originalText;
button.classList.remove("bg-green-600");
}, 2000);
} catch (err) {
console.error("Gagal menyalin:", err);
alert("Maaf, gagal menyalin kode");
}
};
},
};
},
createForm: async function (ret, callback) {
// const hasil = await createForm(ret, callback);
return createForm(ret, callback);
},
// createModal: function () {
// return createModal();
// },
Render: function () {
return {
View: function (row) {
return new View(row);
},
Tabel: function (row) {
return {
Matrix: function (row) {
return new TabelMatrix(row);
},
};
},
SinglePageApp: function (e) {
const spa = new SinglePageApp();
return spa.SinglePageApplication(e);
},
latSinglePageApp: function (result) {
const latSing = latSinglePageApp({
elementById: result.elementById,
endpoint: result.key,
forceReload: false,
data: result.data.data,
})
.then((response) => {
if (response) {
const contentElement = document.getElementById(
result.data.elementById
);
contentElement.innerHTML = response;
} else {
console.log("Data tidak ditemukan");
}
})
.catch((error) => {
console.error("Terjadi kesalahan:", error);
});
return latSing;
},
};
},
Network: function () {
return {
tatiye: function () {
return tatiye;
},
WebSocket: function () {
return tatiye;
},
Queue: function (row) {
return Queue(row);
},
Brief: function (row) {
return Brief(row);
},
Crypto: function () {
return new Crypto();
},
Buckets: function (row) {
return Buckets(row);
},
RTDb: function (callback, token) {
return RTDb(callback, token);
},
filterRow: function (data, propertyMap) {
return filterRow(data, propertyMap);
},
indexDB: function () {
const db = new classIndexDB();
return {
add: async function (row) {
try {
const key = row.key;
const data = row.data;
const timestamp = Date.now();
const hasil = await db.saveData(key, data, timestamp);
const tersimpan = await db.getData(key);
return tersimpan;
} catch (error) {
console.error("Error:", error);
}
},
get: async function (key) {
try {
const data = await db.getData(key);
return data;
} catch (error) {
console.error("Error:", error);
}
},
ref: async function () {
try {
const allData = await db.getAllData();
return allData;
} catch (error) {
console.error("Error:", error);
}
},
up: async function (key, newData) {
try {
await db.updateData(key, newData);
const updatedData = await db.getData(key);
return updatedData;
} catch (error) {
console.error("Error:", error);
}
},
del: async function (key) {
try {
const result = await db.deleteData(key);
return result;
} catch (error) {
console.error("Error:", error);
}
},
latest: async function () {
try {
const latestData = await db.getLatestData();
return latestData;
} catch (error) {
console.error("Error:", error);
}
},
};
},
localStorage: function () {
const storage = new classLocalStorage();
return {
add: async function (row) {
try {
const key = row.key;
const data = row.data;
const timestamp = Date.now();
await storage.saveData(key, data, timestamp);
const tersimpan = await storage.getData(key);
return tersimpan;
} catch (error) {
console.error("Error:", error);
}
},
get: async function (key) {
try {
const data = await storage.getData(key);
return data;
} catch (error) {
console.error("Error:", error);
}
},
ref: async function () {
try {
const allData = await storage.getAllData();
return allData;
} catch (error) {
console.error("Error:", error);
}
},
up: async function (key, newData) {
try {
await storage.updateData(key, newData);
const updatedData = await storage.getData(key);
return updatedData;
} catch (error) {
console.error("Error:", error);
}
},
del: async function (key) {
try {
const result = await storage.deleteData(key);
return result;
} catch (error) {
console.error("Error:", error);
}
},
latest: async function () {
try {
const latestData = await storage.getLatestData();
return latestData;
} catch (error) {
console.error("Error:", error);
}
},
};
},
cookies: function () {
const storage = new classCookies();
return {
add: async function (row, options = {}) {
try {
const key = row.key;
const data = row.data;
const timestamp = Date.now();
await storage.saveData(key, data, timestamp, options);
const tersimpan = await storage.getData(key);
return tersimpan;
} catch (error) {
console.error("Error:", error);
}
},
get: async function (key) {
try {
const data = await storage.getData(key);
return data;
} catch (error) {
console.error("Error:", error);
}
},
ref: async function () {
try {
const allData = await storage.getAllData();
return allData;
} catch (error) {
console.error("Error:", error);
}
},
up: async function (key, newData, options = {}) {
try {
await storage.updateData(key, newData, options);
const updatedData = await storage.getData(key);
return updatedData;
} catch (error) {
console.error("Error:", error);
}
},
del: async function (key, options = {}) {
try {
const result = await storage.deleteData(key, options);
return result;
} catch (error) {
console.error("Error:", error);
}
},
latest: async function () {
try {
const latestData = await storage.getLatestData();
return latestData;
} catch (error) {
console.error("Error:", error);
}
},
};
},
sessionStorage: function () {
const storage = new classSessionStorage();
return {
add: async function (row) {
try {
const key = row.key;
const data = row.data;
const timestamp = Date.now();
await storage.saveData(key, data, timestamp);
const tersimpan = await storage.getData(key);
return tersimpan;
} catch (error) {
console.error("Error:", error);
}
},
get: async function (key) {
try {
const data = await storage.getData(key);
return data;
} catch (error) {
console.error("Error:", error);
}
},
ref: async function () {
try {
const allData = await storage.getAllData();
return allData;
} catch (error) {
console.error("Error:", error);
}
},
up: async function (key, newData) {
try {
await storage.updateData(key, newData);
const updatedData = await storage.getData(key);
return updatedData;
} catch (error) {
console.error("Error:", error);
}
},
del: async function (key) {
try {
const result = await storage.deleteData(key);
return result;
} catch (error) {
console.error("Error:", error);
}
},
latest: async function () {
try {
const latestData = await storage.getLatestData();
return latestData;
} catch (error) {
console.error("Error:", error);
}
},
};
},
};
},
};
}
// Mendefinisikan module pattern untuk Network
if (typeof define === "function" && define.amd) {
// AMD
define([], () => Ngorei);
} else if (typeof module === "object" && module.exports) {
// CommonJS/Node.js
module.exports = Ngorei;
} else {
// Browser global
window.Ngorei = Ngorei;
window.dbs = new Ngorei(); // Instance default
}
//Komponen Network
// Fungsi untuk mengelola cookie di browser
export function cookies(element) {
// Fungsi untuk mengatur cookie
const setCookie = (name, value, days) => {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; path=/";
};
// Fungsi untuk mendapatkan nilai cookie (dengan decode)
const getCookie = (name) => {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) {
// Tambahkan decodeURIComponent untuk decode nilai cookie
return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
}
return null;
};
// Fungsi untuk menghapus cookie
const deleteCookie = (name) => {
document.cookie = name + "=; Max-Age=-99999999;";
};
return {
set: setCookie,
get: getCookie,
delete: deleteCookie,
};
}
const cookieManager = cookies();
let HOSTSOKET; // Deklarasi di luar blok
const HOSTW = window.location.host.split(".")[0];
if (HOSTW === "192") {
HOSTSOKET = window.location.host;
} else {
HOSTSOKET = "127.0.0.1";
}
// Membuat koneksi WebSocket
const WS_HOST = HOSTSOKET;
const WS_PORT = 8080;
export const createWebSocketConnection = () => {
return false;
};
const app = {
app: "Ngorei",
version: "v1.0.4",
copyright: "2013-2024",
vid: cookieManager.get("VID"),
url: window.location.origin,
};
export default app;
//Komponen Network
export class classIndexDB {
constructor(dbName = "Database", dbVersion = 1, storeName = "Data") {
this.dbName = dbName;
this.dbVersion = dbVersion;
this.storeName = storeName;
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = (event) =>
reject("Error membuka database: " + event.target.error);
request.onsuccess = (event) => resolve(event.target.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const objectStore = db.createObjectStore(this.storeName, {
keyPath: "key",
});
objectStore.createIndex("updated_at", "updated_at", { unique: false });
};
});
}
async saveData(key, data, updated_at) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(key);
getRequest.onsuccess = (event) => {
const existingData = event.target.result;
if (existingData && existingData.updated_at >= updated_at) {
resolve("Data sudah yang terbaru");
} else {
const updateRequest = store.put({
key: key,
data: data,
updated_at: updated_at,
});
updateRequest.onsuccess = () =>
resolve("Data berhasil disimpan/diperbarui");
updateRequest.onerror = (event) =>
reject("Error menyimpan/memperbarui data: " + event.target.error);
}
};
getRequest.onerror = (event) =>
reject("Error memeriksa data: " + event.target.error);
});
}
async getData(key) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) =>
reject("Error mengambil data: " + event.target.error);
});
}
async deleteData(key) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.delete(key);
request.onsuccess = () => resolve("Data berhasil dihapus");
request.onerror = (event) =>
reject("Error menghapus data: " + event.target.error);
});
}
async getAllData() {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) =>
reject("Error mengambil semua data: " + event.target.error);
});
}
async updateData(key, newData) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(key);
getRequest.onsuccess = (event) => {
const existingData = event.target.result;
if (!existingData) {
reject("Data tidak ditemukan");
return;
}
const updatedObject = {
key: key,
data: { ...existingData.data, ...newData },
updated_at: Date.now(),
};
const updateRequest = store.put(updatedObject);
updateRequest.onsuccess = () => resolve("Data berhasil diupdate");
updateRequest.onerror = (event) =>
reject("Error mengupdate data: " + event.target.error);
};
getRequest.onerror = (event) =>
reject("Error memeriksa data: " + event.target.error);
});
}
async getLatestData() {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const index = store.index("updated_at");
const request = index.openCursor(null, "prev");
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
resolve(cursor.value);
} else {
resolve(null);
}
};
request.onerror = (event) =>
reject("Error mengambil data terbaru: " + event.target.error);
});
}
}
export class classLocalStorage {
constructor(prefix = "") {
this.prefix = prefix;
}
saveData(key, data, updated_at) {
return new Promise((resolve) => {
const fullKey = this.prefix + key;
const existingData = localStorage.getItem(fullKey);
if (existingData) {
const parsed = JSON.parse(existingData);
if (parsed.updated_at >= updated_at) {
resolve("Data sudah yang terbaru");
return;
}
}
const saveObject = {
key: key,
data: data,
updated_at: updated_at,
};
localStorage.setItem(fullKey, JSON.stringify(saveObject));
resolve("Data berhasil disimpan/diperbarui");
});
}
getData(key) {
return new Promise((resolve) => {
const data = localStorage.getItem(this.prefix + key);
resolve(data ? JSON.parse(data) : null);
});
}
deleteData(key) {
return new Promise((resolve) => {
localStorage.removeItem(this.prefix + key);
resolve("Data berhasil dihapus");
});
}
getAllData() {
return new Promise((resolve) => {
const allData = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(this.prefix)) {
const data = JSON.parse(localStorage.getItem(key));
allData.push(data);
}
}
resolve(allData);
});
}
updateData(key, newData) {
return new Promise((resolve, reject) => {
const fullKey = this.prefix + key;
const existingData = localStorage.getItem(fullKey);
if (!existingData) {
reject("Data tidak ditemukan");
return;
}
const parsed = JSON.parse(existingData);
const updatedObject = {
key: key,
data: { ...parsed.data, ...newData },
updated_at: Date.now(),
};
localStorage.setItem(fullKey, JSON.stringify(updatedObject));
resolve("Data berhasil diupdate");
});
}
async getLatestData() {
return new Promise((resolve) => {
let latestData = null;
let latestTimestamp = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(this.prefix)) {
const data = JSON.parse(localStorage.getItem(key));
if (data.updated_at > latestTimestamp) {
latestTimestamp = data.updated_at;
latestData = data;
}
}
}
resolve(latestData);
});
}
}
export class classCookies {
constructor(prefix = "app_") {
this.prefix = prefix;
}
saveData(key, data, updated_at, options = {}) {
return new Promise((resolve) => {
const fullKey = this.prefix + key;
const existingData = this.getCookie(fullKey);
if (existingData) {
const parsed = JSON.parse(existingData);
if (parsed.updated_at >= updated_at) {
resolve("Data sudah yang terbaru");
return;
}
}
const saveObject = {
key: key,
data: data,
updated_at: updated_at,
};
// Default options
const defaultOptions = {
expires: 365, // hari
path: "/",
secure: false,
sameSite: "Lax",
};
const cookieOptions = { ...defaultOptions, ...options };
// Set expires
let expires = "";
if (cookieOptions.expires) {
const date = new Date();
date.setTime(
date.getTime() + cookieOptions.expires * 24 * 60 * 60 * 1000
);
expires = `expires=${date.toUTCString()};`;
}
// Build cookie string
let cookieString = `${fullKey}=${encodeURIComponent(
JSON.stringify(saveObject)
)};${expires}`;
cookieString += `path=${cookieOptions.path};`;
if (cookieOptions.secure) cookieString += "secure;";
if (cookieOptions.sameSite)
cookieString += `sameSite=${cookieOptions.sameSite};`;
document.cookie = cookieString;
resolve("Data berhasil disimpan/diperbarui");
});
}
getCookie(key) {
const name = this.prefix + key + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(";");
for (let i = 0; i < cookieArray.length; i++) {
let cookie = cookieArray[i].trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length);
}
}
return null;
}
getData(key) {
return new Promise((resolve) => {
const data = this.getCookie(key);
resolve(data ? JSON.parse(data) : null);
});
}
deleteData(key, options = {}) {
return new Promise((resolve) => {
const fullKey = this.prefix + key;
const defaultOptions = {
path: "/",
secure: false,
sameSite: "Lax",
};
const cookieOptions = { ...defaultOptions, ...options };
// Set expired date to past
document.cookie = `${fullKey}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${cookieOptions.path}`;
resolve("Data berhasil dihapus");
});
}
getAllData() {
return new Promise((resolve) => {
const allData = [];
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(";");
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i].trim();
if (cookie.indexOf(this.prefix) === 0) {
const equalPos = cookie.indexOf("=");
const value = cookie.substring(equalPos + 1);
try {
const data = JSON.parse(value);
allData.push(data);
} catch (e) {
console.error("Error parsing cookie:", e);
}
}
}
resolve(allData);
});
}
updateData(key, newData, options = {}) {
return new Promise(async (resolve, reject) => {
const existingData = await this.getData(key);
if (!existingData) {
reject("Data tidak ditemukan");
return;
}
const updatedObject = {
key: key,
data: { ...existingData.data, ...newData },
updated_at: Date.now(),
};
await this.saveData(
key,
updatedObject.data,
updatedObject.updated_at,
options
);
resolve("Data berhasil diupdate");
});
}
async getLatestData() {
return new Promise((resolve) => {
let latestData = null;
let latestTimestamp = 0;
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(";");
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i].trim();
if (cookie.indexOf(this.prefix) === 0) {
const equalPos = cookie.indexOf("=");
const value = cookie.substring(equalPos + 1);
try {
const data = JSON.parse(value);
if (data.updated_at > latestTimestamp) {
latestTimestamp = data.updated_at;
latestData = data;
}
} catch (e) {
console.error("Error parsing cookie:", e);
}
}
}
resolve(latestData);
});
}
}
export class classSessionStorage {
constructor(prefix = "app_") {
this.prefix = prefix;
}
saveData(key, data, updated_at) {
return new Promise((resolve) => {
const fullKey = this.prefix + key;
const existingData = sessionStorage.getItem(fullKey);
if (existingData) {
const parsed = JSON.parse(existingData);
if (parsed.updated_at >= updated_at) {
resolve("Data sudah yang terbaru");
return;
}
}
const saveObject = {
key: key,
data: data,
updated_at: updated_at,
};
sessionStorage.setItem(fullKey, JSON.stringify(saveObject));
resolve("Data berhasil disimpan/diperbarui");
});
}
getData(key) {
return new Promise((resolve) => {
const data = sessionStorage.getItem(this.prefix + key);
resolve(data ? JSON.parse(data) : null);
});
}
deleteData(key) {
return new Promise((resolve) => {
sessionStorage.removeItem(this.prefix + key);
resolve("Data berhasil dihapus");
});
}
getAllData() {
return new Promise((resolve) => {
const allData = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key.startsWith(this.prefix)) {
const data = JSON.parse(sessionStorage.getItem(key));
allData.push(data);
}
}
resolve(allData);
});
}
updateData(key, newData) {
return new Promise((resolve, reject) => {
const fullKey = this.prefix + key;
const existingData = sessionStorage.getItem(fullKey);
if (!existingData) {
reject("Data tidak ditemukan");
return;
}
const parsed = JSON.parse(existingData);
const updatedObject = {
key: key,
data: { ...parsed.data, ...newData },
updated_at: Date.now(),
};
sessionStorage.setItem(fullKey, JSON.stringify(updatedObject));
resolve("Data berhasil diupdate");
});
}
async getLatestData() {
return new Promise((resolve) => {
let latestData = null;
let latestTimestamp = 0;
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key.startsWith(this.prefix)) {
const data = JSON.parse(sessionStorage.getItem(key));
if (data.updated_at > latestTimestamp) {
latestTimestamp = data.updated_at;
latestData = data;
}
}
}
resolve(latestData);
});
}
}
/**
* @class Ngorei
* @description Kelas utama untuk manajemen komponen dan DOM
*/
export class NgoreiDOM {
constructor() {
this.DOM = new TDSDOM();
this.Components = function () {
return {};
};
}
}
// Export instance WebSocket default
export const tatiye = false;
export function RTDb(callback, token) {
// let pesanData;
// tatiye.onopen = function() {
// const subscribeMsg = {
// type: 'subscribe',
// endpoint:token
// };
// tatiye.send(JSON.stringify(subscribeMsg));
// };
// tatiye.onmessage = function(event) {
// const data = JSON.parse(event.data);
// if (data.type === 'update') {
// pesanData = data.data.response;
// callback(pesanData);
// }
// };
// tatiye.onerror = function(error) {
// console.error('WebSocket error:', error);
// };
// tatiye.onclose = function() {
// console.log('Terputus dari WebSocket server');
// setTimeout(RTDb, 5000);
// };
}
// ... existing connection code ...
export function Buckets(data = {}) {
// return new Promise((resolve, reject) => {
// if (tatiye.readyState === WebSocket.OPEN) {
// const apiRequest = {
// type: 'apiRequest',
// endpoint: data.endpoint,
// vid:app.vid,
// payload: data.body
// };
// const messageHandler = (e) => {
// try {
// const response = JSON.parse(e.data);
// if (response.type === "apiResponse") {
// tatiye.removeEventListener("message", messageHandler);
// if (response.data.payload.vid===app.vid) {
// resolve(response.data.payload.response);
// }
// }
// } catch (error) {
// console.error('Error dalam messageHandler:', error);
// reject(error);
// }
// };
// tatiye.addEventListener("message", messageHandler);
// tatiye.send(JSON.stringify(apiRequest));
// } else {
// console.error('WebSocket Status:', tatiye.readyState);
// reject(new Error("WebSocket belum terhubung"));
// }
// });
}
// filebrowser
export async function filebrowser(serverUrl, fileInput, additionalData = {}) {
const formdata = new FormData();
formdata.append("file", fileInput.files[0]);
// Menambahkan data tambahan secara dinamis
Object.entries(additionalData).forEach(([key, value]) => {
formdata.append(key, value);
});
const requestOptions = {
method: "POST",
body: formdata,
redirect: "follow",
};
try {
const response = await fetch(app.url + "/sdk/" + serverUrl, requestOptions);
const result = await response.json();
return result;
} catch (error) {
console.error("Error uploading file:", error);
throw error;
}
}
// PRECODE
export function getLanguageIcon(language) {
if (!language || typeof language !== "string") {
return "fas fa-code";
}
const iconMap = {
html: "fab fa-html5",
css: "fab fa-css3-alt",
js: "fab fa-js",
javascript: "fab fa-js",
python: "fab fa-python",
php: "fab fa-php",
java: "fab fa-java",
react: "fab fa-react",
vue: "fab fa-vuejs",
angular: "fab fa-angular",
node: "fab fa-node-js",
sass: "fab fa-sass",
wordpress: "fab fa-wordpress",
git: "fab fa-git-alt",
json: "fas fa-code",
wrapped: "fas fa-code",
};
return iconMap[language.toLowerCase()] || "fas fa-code"; // Default icon
}
// PRE CODE
export function wrapCodeWithTerminal() {
const codeBlocks = document.querySelectorAll("pre > code:not(.wrapped)");
codeBlocks.forEach((codeBlock) => {
// Tandai kode yang sudah diproses
// codeBlock.classList.add("wrapped");
const classAttr = codeBlock.className;
const language = classAttr.replace("language-", "").replace("wrapped", ""); // Hapus class wrapped dari string language
const title = codeBlock.getAttribute("title") || "";
const setIcon = codeBlock.getAttribute("icon") || language;
const defaultHeight = 400; // Nilai default dalam pixel
const setmaxHeight =
parseInt(codeBlock.getAttribute("maxHeight")) || defaultHeight;
const defaultwidth = ""; // Nilai default dalam pixel
const setmaxWidth =
parseInt(codeBlock.getAttribute("maxwidth")) || defaultwidth;
const pxmaxWidth = setmaxWidth + "px"; // Gunakan variabel yang sudah dikonversi
const pxmaxHeight = setmaxHeight + "px"; // Gunakan variabel yang sudah dikonversi
const languageIcon = getLanguageIcon(setIcon);
let newsTitiel = "";
if (title) {
newsTitiel = title + "." + setIcon;
} else {
newsTitiel = language;
}
const terminal = document.createElement("div");
terminal.className = "terminal";
terminal.style.width = pxmaxWidth;
const terminalHeader = document.createElement("div");
terminalHeader.className = "terminal-header";
terminalHeader.innerHTML = `
<span>
<i class="${languageIcon}" aria-hidden="true"></i>
${newsTitiel}
</span>
<div class="terminal-buttons">
<button onclick="copyCode(this)" class="terminal-copy-btn" aria-label="Salin kode">
<i class="icon-feather-copy" aria-hidden="true"></i>
</button>
</div>
`;
const terminalFooter = document.createElement("div");
terminalFooter.className = "terminal-footer";
if (codeBlock.offsetHeight > setmaxHeight) {
terminalFooter.innerHTML = `
<button class="terminal-code-btn" onclick="toggleCode(this)">Lihat selengkapnya</button>
`;
}
const preElement = codeBlock.parentElement;
preElement.parentNode.insertBefore(terminal, preElement);
terminal.appendChild(terminalHeader);
terminal.appendChild(preElement);
terminal.appendChild(terminalFooter);
if (codeBlock.offsetHeight > setmaxHeight) {
preElement.style.maxHeight = pxmaxHeight;
preElement.style.overflow = "hidden";
}
});
}
window.toggleCode = function (button) {
const terminal = button.closest(".terminal");
const preElement = terminal.querySelector("pre");
const codeElement = preElement.querySelector("code");
const defaultHeight = 300;
const maxHeight =
(parseInt(codeElement.getAttribute("maxHeight")) || defaultHeight) + "px";
if (preElement.style.maxHeight === maxHeight) {
preElement.style.maxHeight = "none";
button.textContent = "Lihat lebih sedikit";
} else {
preElement.style.maxHeight = maxHeight;
button.textContent = "Lihat selengkapnya";
}
};
export function md5(string) {
function RotateLeft(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
}
function AddUnsigned(lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = lX & 0x80000000;
lY8 = lY & 0x80000000;
lX4 = lX & 0x40000000;
lY4 = lY & 0x40000000;
lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
if (lX4 & lY4) {
return lResult ^ 0x80000000 ^ lX8 ^ lY8;
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
} else {
return lResult ^ 0x40000000 ^ lX8 ^ lY8;
}
} else {
return lResult ^ lX8 ^ lY8;
}
}
function F(x, y, z) {
return (x & y) | (~x & z);
}
function G(x, y, z) {
return (x & z) | (y & ~z);
}
function H(x, y, z) {
return x ^ y ^ z;
}
function I(x, y, z) {
return y ^ (x | ~z);
}
function FF(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
}
function GG(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
}
function HH(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
}
function II(a, b, c, d, x, s, ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
}
function ConvertToWordArray(string) {
var lWordCount;
var lMessageLength = string.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 =
(lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] =
lWordArray[lWordCount] |
(string.charCodeAt(lByteCount) << lBytePosition);
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function WordToHex(lValue) {
var WordToHexValue = "",
WordToHexValue_temp = "",
lByte,
lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
WordToHexValue_temp = "0" + lByte.toString(16);
WordToHexValue =
WordToHexValue +
WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
}
return WordToHexValue;
}
function Utf8Encode(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
var x = Array();
var k, AA, BB, CC, DD, a, b, c, d;
var S11 = 7,
S12 = 12,
S13 = 17,
S14 = 22;
var S21 = 5,
S22 = 9,
S23 = 14,
S24 = 20;
var S31 = 4,
S32 = 11,
S33 = 16,
S34 = 23;
var S41 = 6,
S42 = 10,
S43 = 15,
S44 = 21;
string = Utf8Encode(string);
x = ConvertToWordArray(string);
a = 0x67452301;
b = 0xefcdab89;
c = 0x98badcfe;
d = 0x10325476;
for (k = 0; k < x.length; k += 16) {
AA = a;
BB = b;
CC = c;
DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);
b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);
b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
d = GG(d, a, b, c, x[k + 10], S22, 0x02441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x04881d05);
a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);
c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);
b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);
a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);
d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);
b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);
a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);
d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);
c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);
a = AddUnsigned(a, AA);
d = AddUnsigned(d, DD);
c = AddUnsigned(c, CC);
b = AddUnsigned(b, BB);
}
var temp = WordToHex(a) + WordToHex(d) + WordToHex(c) + WordToHex(b);
return temp.toUpperCase();
}
export function md5Str(input) {
const hash = md5(input);
const codes = [];
// Membagi hash menjadi 5 bagian yang sama (6 karakter)
for (let i = 0; i < 5; i++) {
codes.push(hash.substr(i * 6, 6));
}
return codes.join("-");
}
// Fungsi untuk melakukan HTTP request dengan fitur retry, timeout, dan validasi
export function BriefStorage(element) {
// Konstanta untuk timeout
const TIMEOUT_DURATION = 5000;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
// Validasi URL
const validateUrl = (url) => {
try {
new URL(url);
return true;
} catch {
throw new Error("URL tidak valid");
}
};
// Rate limiting
const rateLimiter = {
lastCall: 0,
minInterval: 5, // 100ms antara request
checkLimit() {
const now = Date.now();
if (now - this.lastCall < this.minInterval) {
throw new Error("Terlalu banyak request. Mohon tunggu sebentar.");
}
this.lastCall = now;
},
};
// Fungsi helper untuk setup request
const setupRequest = (method, data = null) => {
const controller = new AbortController();
const config = {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
signal: controller.signal,
};
if (data) {
if (typeof data !== "object") {
throw new Error("Data harus berupa object");
}
config.body = JSON.stringify(data);
}
return { controller, config };
};
// Fungsi retry
const retry = async (fn, retries = MAX_RETRIES) => {
try {
return await fn();
} catch (error) {
if (retries <= 1) throw error;
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
console.log(`Mencoba kembali... Sisa percobaan: ${retries - 1}`);
return retry(fn, retries - 1);
}
};
async function getData(url) {
validateUrl(url);
rateLimiter.checkLimit();
const { controller, config } = setupRequest("GET");
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_DURATION);
return retry(async () => {
try {
const response = await fetch(url, config);
clearTimeout(timeoutId);
if (!response.ok) {
throw {
type: "HTTPError",
status: response.status,
message: `HTTP error! status: ${response.status}`,
timestamp: new Date().toISOString(),
url,
};
}
const data = await response.json();
console.log({
type: "Success",
method: "GET",
url,
timestamp: new Date().toISOString(),
});
return data;
} catch (error) {
clearTimeout(timeoutId);
// console.error({
// type: "Error",
// method: "GET",
// url,
// error: error.message,
// timestamp: new Date().toISOString(),
// });
throw error;
}
});
}
async function sdk(url, data) {
// const cookieManager = cookies();
// const userCookie = cookieManager.get('HOST'); // returns 'john'
const bseURI = app.url + "/sdk/" + url;
validateUrl(bseURI);
rateLimiter.checkLimit();
const { controller, config } = setupRequest("POST", data);
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_DURATION);
return retry(async () => {
try {
const response = await fetch(bseURI, config);
clearTimeout(timeoutId);
if (!response.ok) {
throw {
type: "HTTPError",
status: response.status,
message: `HTTP error! status: ${response.status}`,
timestamp: new Date().toISOString(),
url,
data,
};
}
const result = await response.json();
return result;
} catch (error) {
clearTimeout(timeoutId);
console.error({
type: "Error",
method: "POST",
url,
error: error.message,
data,
timestamp: new Date().toISOString(),
});
throw error;
}
});
}
return { element, getData, sdk };
}
export async function Brief(row) {
try {
if (!row || !row.endpoint) {
throw new Error("Parameter row dan endpoint diperlukan");
}
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row.endpoint, row);
return data;
} catch (error) {
console.error("Error dalam Brief:", error);
throw error;
}
}
export function Queue(row) {
return {
add: async function () {
try {
if (!row || !row.endpoint) {
throw new Error("Parameter dan endpoint diperlukan");
}
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row.endpoint, row);
return data;
} catch (error) {
console.error("Error dalam Queue:", error);
throw error;
}
},
up: async function (id) {
try {
if (!row || !row.endpoint) {
throw new Error("Parameter dan endpoint diperlukan");
}
const gabungArray = { ...row, id: id };
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row.endpoint, gabungArray);
return { ...row.payload, id: id, data };
} catch (error) {
console.error("Error dalam Queue:", error);
throw error;
}
},
get: async function (id) {
try {
if (!row || !row) {
throw new Error("Parameter dan endpoint diperlukan");
}
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row, { id: id });
return data;
} catch (error) {
console.error("Error dalam Queue:", error);
throw error;
}
},
view: async function () {
try {
if (!row || !row.endpoint) {
throw new Error("Parameter dan endpoint diperlukan");
}
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row.endpoint, row);
return data;
} catch (error) {
console.error("Error dalam Queue:", error);
throw error;
}
},
del: async function (id) {
try {
if (!row || !row) {
throw new Error("Parameter dan endpoint diperlukan");
}
const briefInstance = BriefStorage(row);
const data = await briefInstance.sdk(row, { id: id });
return data;
} catch (error) {
console.error("Error dalam Queue:", error);
throw error;
}
},
};
}
// COMPONENE
/**
* @class TDSDOM
* @description Kelas untuk manajemen DOM dan template
*/
class TDSDOM {
constructor() {
/**
* Helper function untuk escape karakter regex
* @param {string} string - String yang akan di-escape
* @returns {string} String yang sudah di-escape
*/
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
/**
* Render template dengan data
* @param {string} template - Template string
* @param {Object} data - Data untuk dirender
* @param {Element} element - Element template
*/
this.render = function (template, data, element) {
try {
// Validasi input
if (!template || typeof template !== "string") {
throw new Error("Template harus berupa string");
}
if (!data || typeof data !== "object") {
throw new Error("Data harus berupa object");
}
let result = template;
const dataKeys = Object.keys(data);
// Proses setiap key data
dataKeys.forEach((key) => {
const items = data[key];
if (!Array.isArray(items)) {
console.warn(`Data untuk key ${key} bukan array:`, items);
return;
}
// Pattern untuk mencari tag template
// Support multiple format tags termasuk Mustache-style
const startTags = [
`{@${key}}`,
`[${key}]`,
`[@${key}]`,
`<!--${key}-->`,
`{{${key}}}`, // Mustache-style
`{{#${key}}}`, // Mustache block
`{{{${key}}}`, // Mustache unescaped
`{$${key}}`, // PHP-style variable
`{#${key}}`, // Hash-style variable
`\${${key}}`, // Template literal style
];
const endTags = [
`{/${key}}`,
`[/${key}]`,
`[/${key}]`,
`<!--/${key}-->`,
`{{/${key}}}`, // Mustache-style
`{{/${key}}}`, // Mustache block
`{{{/${key}}}`, // Mustache unescaped
`{/${key}}`, // PHP-style closing
`{/${key}}`, // Hash-style closing
`\${/${key}}`, // Template literal style closing
];
// Debug log
// Cek format yang digunakan
let templateStart = -1;
let templateEnd = -1;
let usedStartTag = "";
let usedEndTag = "";
let tagFound = false;
// Cek format mana yang digunakan dengan case insensitive
for (let i = 0; i < startTags.length; i++) {
const startTagRegex = new RegExp(escapeRegExp(startTags[i]), "i");
const startMatch = result.match(startTagRegex);
if (startMatch) {
templateStart = startMatch.index;
usedStartTag = startMatch[0];
const endTagRegex = new RegExp(escapeRegExp(endTags[i]), "i");
const remainingContent = result.slice(
templateStart + usedStartTag.length
);
const endMatch = remainingContent.match(endTagRegex);
if (endMatch) {
templateEnd =
templateStart + usedStartTag.length + endMatch.index;
usedEndTag = endMatch[0];
tagFound = true;
//console.debug(`Tag ditemukan: ${usedStartTag} ... ${usedEndTag}`);
break;
}
}
}
if (!tagFound) {
console.warn(
`Tag template untuk "${key}" tidak ditemukan dalam template`
);
console.warn("Template yang tersedia:", result);
return;
}
const itemTemplate = result.substring(
templateStart + usedStartTag.length,
templateEnd
);
// console.debug(`Template item untuk "${key}":`, itemTemplate);
// Render setiap item dengan dukungan Mustache yang lebih baik
let renderedItems = items
.map((item) => {
let itemResult = itemTemplate;
// Replace semua placeholder dengan nilai item
Object.keys(item).forEach((prop) => {
const value = item[prop] ?? "";
// Support multiple format placeholders termasuk Mustache
const patterns = [
new RegExp(`{${prop}}`, "g"),
new RegExp(`\\[${prop}\\]`, "g"),
new RegExp(`<!--${prop}-->`, "g"),
new RegExp(`{{${prop}}}`, "g"), // Mustache escaped
new RegExp(`{{{${prop}}}}`, "g"), // Mustache unescaped
new RegExp(`{{&${prop}}}`, "g"), // Mustache unescaped alternative
new RegExp(`{\\$${prop}}`, "g"), // PHP-style variable
new RegExp(`{#${prop}}`, "g"), // Hash-style variable
new RegExp(`\\$\\{${prop}\\}`, "g"), // Template literal style
];
patterns.forEach((pattern) => {
// Untuk format Mustache escaped, escape HTML
if (
pattern.toString().includes("{{") &&
!pattern.toString().includes("{{{")
) {
itemResult = itemResult.replace(
pattern,
this.sanitize(String(value))
);
} else {
itemResult = itemResult.replace(pattern, String(value));
}
});
});
return itemResult;
})
.join("");
// Replace template dengan hasil render
result = result.replace(
`${usedStartTag}${itemTemplate}${usedEndTag}`,
renderedItems
);
});
// Debug log hasil akhir
return result;
} catch (error) {
console.error("Error dalam render:", error);
console.error("Template:", template);
console.error("Data:", data);
return "";
}
};
/**
* Parse string template menjadi DOM elements
* @param {string} template - Template string
* @returns {DocumentFragment}
*/
this.parse = function (template) {
const parser = new DOMParser();
const doc = parser.parseFromString(template, "text/html");
const fragment = document.createDocumentFragment();
while (doc.body.firstChild) {
fragment.appendChild(doc.body.firstChild);
}
return fragment;
};
/**
* Sanitize string untuk mencegah XSS
* @param {string} str - String yang akan disanitize
* @returns {string}
*/
this.sanitize = function (str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
};
}
}
// SPA
export class SinglePageApp {
constructor() {
this.dbName = "spaCache";
this.storeName = "responses";
this.db = null;
this.loadingHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Memuat konten...</p>
</div>
`;
this.retryConfig = {
maxRetries: 3,
retryDelay: 1000,
backoffMultiplier: 1.5,
};
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: "id" });
store.createIndex("timestamp", "timestamp", { unique: false });
}
};
});
}
isCryptoSupported() {
return window.crypto && window.crypto.subtle;
}
async getEncryptionKey(endpoint) {
const keyData = new TextEncoder().encode(
endpoint || "default-endpoint-key"
);
return await crypto.subtle.importKey("raw", keyData, "AES-GCM", false, [
"encrypt",
"decrypt",
]);
}
async encryptData(data, endpoint) {
if (!this.isCryptoSupported()) {
return { raw: true, data };
}
try {
const key = await this.getEncryptionKey(endpoint);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encodedData = new TextEncoder().encode(data);
const encryptedData = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
encodedData
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedData)),
};
} catch (error) {
console.warn("Enkripsi gagal:", error);
return { raw: true, data };
}
}
async decryptData(encryptedObj, endpoint) {
if (encryptedObj.raw) {
return encryptedObj.data;
}
try {
const key = await this.getEncryptionKey(endpoint);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: new Uint8Array(encryptedObj.iv) },
key,
new Uint8Array(encryptedObj.data)
);
return new TextDecoder().decode(decrypted);
} catch (error) {
console.warn("Dekripsi gagal:", error);
throw error;
}
}
async saveToCache(key, data, endpoint) {
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
try {
const objToEncrypt = JSON.stringify({
data: data,
timestamp: new Date().getTime(),
});
const encryptedData = await this.encryptData(objToEncrypt, endpoint);
await store.put({
id: key,
encryptedContent: encryptedData,
});
} catch (error) {
console.warn("Cache write error:", error);
}
}
async getFromCache(key) {
const transaction = this.db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
return new Promise(async (resolve, reject) => {
try {
const request = store.get(key);
request.onsuccess = async () => {
if (!request.result) {
resolve(null);
return;
}
const decryptedStr = await this.decryptData(
request.result.encryptedContent
);
const decryptedObj = JSON.parse(decryptedStr);
resolve({
id: key,
data: decryptedObj.data,
timestamp: decryptedObj.timestamp,
});
};
request.onerror = () => reject(request.error);
} catch (error) {
console.warn("Cache read error:", error);
reject(error);
}
});
}
async clearOldCache() {
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 hari
const now = new Date().getTime();
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const index = store.index("timestamp");
const range = IDBKeyRange.upperBound(now - maxAge);
index.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
store.delete(cursor.primaryKey);
cursor.continue();
}
};
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async fetchWithRetry(url, options, retryCount = 0) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (retryCount >= this.retryConfig.maxRetries) {
throw new Error(
`Gagal setelah ${this.retryConfig.maxRetries} percobaan: ${error.message}`
);
}
const waitTime =
this.retryConfig.retryDelay *
Math.pow(this.retryConfig.backoffMultiplier, retryCount);
console.warn(
`Percobaan ke-${
retryCount + 1
} gagal. Mencoba ulang dalam ${waitTime}ms...`
);
await this.delay(waitTime);
return this.fetchWithRetry(url, options, retryCount + 1);
}
}
async SinglePageApplication(e) {
const encodedData = btoa(JSON.stringify(e));
const State = {
url: "https://" + e.endpoint,
data: e.data || e,
elementById: e.elementById,
encodedData: encodedData,
endpoint: e.endpoint,
};
if (!this.db) {
await this.initDB();
}
const contentElement = document.getElementById(State.elementById);
contentElement.innerHTML = this.loadingHTML;
const cacheKey = md5Str(State.encodedData + "_v1");
try {
const cachedData = await this.getFromCache(cacheKey);
if (cachedData) {
contentElement.innerHTML = cachedData.data;
return cachedData.data;
}
} catch (error) {
console.warn("Cache read error:", error);
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await this.fetchWithRetry(
app.url + "/worker/" + cacheKey,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...State.data,
key: md5Str(State.url) || "",
brief: State.url || "",
pageparser: window.location.href,
timestamp: new Date().getTime(),
}),
signal: controller.signal,
}
);
clearTimeout(timeoutId);
const responseData = await response.text();
try {
await this.saveToCache(cacheKey, responseData, State.endpoint);
await this.clearOldCache();
} catch (e) {
console.warn("Cache write error:", e);
}
contentElement.innerHTML = responseData;
return responseData;
} catch (error) {
const errorMessage = error.message.includes("Gagal setelah")
? error.message
: `Error: ${error.message}`;
contentElement.innerHTML = `<div class="error">${errorMessage}</div>`;
return contentElement.innerHTML;
}
}
}
// // Inisialisasi Worker
// const worker = new Worker(app.url + "/js/Worker.js");
// // Fungsi untuk menggunakan Worker
// const useWorker = (action, data) => {
// return new Promise((resolve, reject) => {
// worker.onmessage = (e) => {
// const { action: responseAction, result, error } = e.data;
// if (responseAction === `${action}Complete`) {
// resolve(result);
// } else if (responseAction === "error") {
// reject(new Error(error));
// }
// };
// worker.onerror = (error) => {
// reject(error);
// };
// worker.postMessage({ action, data });
// });
// };
// // Contoh penggunaan Worker untuk filter
// async function filterItems(items, filters) {
// try {
// const filteredItems = await useWorker("filter", { items, filters });
// console.log("Filtered items:", filteredItems);
// return filteredItems;
// } catch (error) {
// console.error("Error filtering items:", error);
// }
// }
// // Contoh penggunaan Worker untuk search
// async function searchItems(items, query, searchFields) {
// try {
// const searchResults = await useWorker("search", {
// items,
// query,
// searchFields,
// });
// console.log("Search results:", searchResults);
// return searchResults;
// } catch (error) {
// console.error("Error searching items:", error);
// }
// }
export async function latSinglePageApp(e) {
// Inisialisasi Worker $dataset['pageparser']
const worker = new Worker(app.url + "/js/Worker.js");
const encodedData = btoa(JSON.stringify(e));
const State = {
url: "https://" + e.endpoint,
data: e.data || e,
elementById: e.elementById,
encodedData: encodedData,
endpoint: e.endpoint,
};
try {
const response = await fetch(app.url + "/worker/" + md5Str(encodedData), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...State.data,
key: md5Str(State.url) || "",
brief: State.url || "",
pageparser: window.location.href,
timestamp: new Date().getTime(),
}),
});
return await response.text();
} catch (error) {
console.error("Error:", error);
return null;
} finally {
// Terminate worker setelah selesai
worker.terminate();
}
}
// AND SPA
export function Encode(argument) {
if (!argument) {
throw new Error("Input tidak boleh kosong");
}
try {
// Konversi input ke string jika bukan string
const inputString =
typeof argument === "string" ? argument : JSON.stringify(argument);
// Encode ke Base64 dan buat URL-safe
const encodedString = btoa(inputString)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
return encodedString;
} catch (error) {
throw new Error(`Gagal mengencode data: ${error.message}`);
}
}
// URL-safe Base64 Decode
export function Decode(argument) {
if (!argument || typeof argument !== "string") {
throw new Error("Input harus berupa string");
}
try {
// Konversi ke format Base64 standar
const paddedString = argument.replace(/-/g, "+").replace(/_/g, "/");
const padding = (4 - (paddedString.length % 4)) % 4;
const base64String =
padding > 0 ? paddedString + "=".repeat(padding) : paddedString;
// Decode Base64
const decodedString = atob(base64String);
// Bersihkan dan parse JSON
const cleanedData = decodedString
.replace(/'/g, '"')
.replace(/([{,]\s*)(\w+):/g, '$1"$2":');
return JSON.parse(cleanedData);
} catch (error) {
throw new Error(`Gagal mendecode data: ${error.message}`);
}
}
//Tabel Matrix
export class TabelMatrix {
constructor(options) {
this.options = options;
this.currentPage = 1;
this.paginationId = options.pagination || "pagination";
this.paginationPosition = options.paginationPosition || "center";
this.searchId = options.search;
this.exportOptions = options.export || {};
// Pastikan data tersedia sebelum menyalin
if (options.data) {
this.data = {
columns: [...(options.data.columns || [])],
data: options.data.data
? JSON.parse(JSON.stringify(options.data.data))
: [],
};
this.originalData = {
columns: [...(options.data.columns || [])],
data: options.data.data
? JSON.parse(JSON.stringify(options.data.data))
: [],
};
} else {
this.data = { columns: [], data: [] };
this.originalData = { columns: [], data: [] };
}
this.virtualScrolling = options.virtualScrolling || false;
this.chunkSize = options.chunkSize || 100;
this.bufferSize = options.bufferSize || 50;
this.rowHeight = options.rowHeight || 40;
this.memoryLimit = options.memoryLimit || 100000;
this.visibleRows = [];
this.scrollTop = 0;
this.lastScrollTop = 0;
this.scrollThrottle = null;
this.renderRequestId = null;
this.isScrolling = false;
this.requiredLibraries = {
xlsx: {
name: "XLSX",
url: "https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js",
},
pdf: {
name: "jspdf",
url: "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js",
},
autoTable: {
name: "jspdf-autotable",
url: "https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js",
depends: "pdf",
},
};
this.headerStyles = {
backgroundColor: options.headerBackgroundColor || "#e5e9f2",
color: options.headerTextColor || "#000",
fontSize: options.headerFontSize || "14px",
fontWeight: options.headerFontWeight || "700",
};
this.searchIndex = {};
this.searchableColumns = options.searchableColumns || [];
// Pindahkan buildSearchIndex setelah data diinisialisasi
if (this.data && this.data.columns) {
this.buildSearchIndex();
}
this.filters = options.filters || {};
this.columnFormatters = options.columnFormatters || {};
// Tambahkan properti baru untuk menangani kolom yang dapat diedit
this.editableColumns = options.editableColumns || {};
// Properti untuk menyimpan konfigurasi editor yang sedang aktif
this.activeEditor = null;
this.init();
this.setupFilters();
this.setupExportOptions();
}
init() {
const tabelSet = document.getElementById(this.options.containerId);
if (!tabelSet) return;
this.data = this.options.data;
this.perPage = this.options.perPage || 10;
this.currentPage = 1;
this.sortDirection = {};
this.setupExportOptions();
if (this.virtualScrolling) {
//console.log('Virtual scrolling aktif');
this.setupVirtualScrolling();
} else {
this.createTable();
}
this.setupSearch();
if (!this.virtualScrolling) {
this.createPagination();
}
this.setupExportButtons();
this.setupErrorHandling();
}
setupErrorHandling() {
window.onerror = (message, source, lineno, colno, error) => {
// console.error('Terjadi kesalahan:', message, 'di', source, 'baris', lineno);
};
}
sortTable(columnField) {
const direction = this.sortDirection[columnField] || "asc";
const multiplier = direction === "asc" ? 1 : -1;
this.data.data.sort((a, b) => {
if (a[columnField] < b[columnField]) return -1 * multiplier;
if (a[columnField] > b[columnField]) return 1 * multiplier;
return 0;
});
this.sortDirection[columnField] = direction === "asc" ? "desc" : "asc";
this.createTable();
}
createTable() {
if (this.virtualScrolling) {
if (!this.tableWrapper) return;
this.tableWrapper.innerHTML = "";
const table = this.createTableElement();
const thead = this.createTableHeader();
const tbody = document.createElement("tbody");
table.appendChild(thead);
table.appendChild(tbody);
this.tableWrapper.appendChild(table);
this.handleScroll(this.scrollWrapper);
this.updateSortIcons();
} else {
this.clearContainer();
const table = this.createTableElement();
const thead = this.createTableHeader();
const tbody = this.createTableBody();
table.appendChild(thead);
table.appendChild(tbody);
this.appendTableToContainer(table);
this.updateSortIcons();
this.updatePaginationInfo();
}
}
clearContainer() {
const container = document.getElementById(this.options.containerId);
container.innerHTML = "";
}
createTableElement() {
const table = document.createElement("table");
table.style = "width:100%;";
return table;
}
createTableHeader() {
const thead = document.createElement("thead");
const headerRow1 = document.createElement("tr");
const headerRow2 = document.createElement("tr");
const numberHeader = document.createElement("th");
this.applyStyles(numberHeader, {
backgroundColor: this.headerStyles.backgroundColor,
color: this.headerStyles.color,
fontSize: this.headerStyles.fontSize,
fontWeight: this.headerStyles.fontWeight,
textAlign: "center",
verticalAlign: "middle",
padding: "10px",
border: "1px solid #ccc",
});
numberHeader.rowSpan = 2;
numberHeader.innerHTML = "No";
headerRow1.appendChild(numberHeader);
this.data.columns.forEach((column) => {
const th = document.createElement("th");
this.applyStyles(th, {
backgroundColor: this.headerStyles.backgroundColor,
color: this.headerStyles.color,
fontSize: this.headerStyles.fontSize,
fontWeight: this.headerStyles.fontWeight,
textAlign: "center",
verticalAlign: "middle",
padding: "10px",
border: "1px solid #ccc",
cursor: "pointer",
});
if (column.columns) {
th.colSpan = column.columns.filter(
(subCol) => subCol.colum !== false
).length;
th.rowSpan = 1;
th.innerHTML = `${
column.title.split("\n")[0]
} <span class="sort-icon pull-right" data-field="${
column.field
}"></span>`;
headerRow1.appendChild(th);
column.columns.forEach((subCol) => {
// Skip kolom jika colum adalah false
if (subCol.colum === false) return;
const subTh = document.createElement("th");
subTh.innerHTML = `${
subCol.title.split("\n")[0]
} <span class="sort-icon pull-right" data-field="${
subCol.field
}"></span>`;
// Tambahkan class jika ada
if (subCol.class) {
subTh.className = subCol.class;
}
this.applyStyles(subTh, {
backgroundColor: this.headerStyles.backgroundColor,
color: this.headerStyles.color,
fontSize: this.headerStyles.fontSize,
fontWeight: this.headerStyles.fontWeight,
textAlign: "center",
verticalAlign: "middle",
padding: "10px",
border: "1px solid #ccc",
cursor: "pointer",
});
subTh.onclick = () => this.sortTable(subCol.field);
headerRow2.appendChild(subTh);
});
} else {
th.rowSpan = 2;
th.colSpan = 1;
th.innerHTML = `${
column.title.split("\n")[0]
} <span class="sort-icon pull-right" data-field="${
column.field
}"></span>`;
th.onclick = () => this.sortTable(column.field);
headerRow1.appendChild(th);
// Tambahkan class jika ada
if (column.class) {
th.className = column.class;
}
}
});
thead.appendChild(headerRow1);
thead.appendChild(headerRow2);
return thead;
}
createTableBody() {
const tbody = document.createElement("tbody");
const startIndex = (this.currentPage - 1) * this.perPage;
const endIndex = startIndex + this.perPage;
const visibleData = this.data.data.slice(startIndex, endIndex);
visibleData.forEach((rowData, index) => {
const row = document.createElement("tr");
// Sel nomor
const numberCell = document.createElement("td");
numberCell.innerText = startIndex + index + 1;
this.applyStyles(numberCell, {
textAlign: "center",
fontSize: "13px",
fontWeight: "400",
backgroundColor: "#ffffff",
padding: "10px",
border: "1px solid #ccc",
verticalAlign: "top",
});
row.appendChild(numberCell);
// Iterasi kolom
this.data.columns.forEach((column) => {
if (column.columns) {
column.columns.forEach((subColumn) => {
// Skip kolom jika colum adalah false
if (subColumn.colum === false) return;
const td = this.createDataCell(rowData, subColumn, index);
row.appendChild(td);
});
} else {
const td = this.createDataCell(rowData, column, index);
row.appendChild(td);
}
});
tbody.appendChild(row);
});
return tbody;
}
appendTableToContainer(table) {
const container = document.getElementById(this.options.containerId);
container.appendChild(table);
}
updateSortIcons() {
const icons = document.querySelectorAll(".sort-icon");
icons.forEach((icon) => {
const field = icon.getAttribute("data-field");
icon.innerHTML = "";
if (this.sortDirection[field]) {
icon.innerHTML =
this.sortDirection[field] === "asc"
? ' <i class="icon-feather-chevron-up"></i>'
: ' <i class="icon-feather-chevron-down"></i>';
}
});
}
setupSearch() {
if (this.searchId) {
const searchInput = document.getElementById(this.searchId);
if (searchInput) {
//console.log('Search input ditemukan:', this.searchId);
searchInput.addEventListener("input", (e) => {
//console.log('Mencari:', e.target.value);
this.performSearch(e.target.value);
});
} else {
console.warn(
`Elemen pencarian dengan id "${this.searchId}" tidak ditemukan.`
);
}
}
}
buildSearchIndex() {
//console.log('Building search index for columns:', this.searchableColumns);
// Pastikan data tersedia sebelum membangun index
if (!this.data || !this.data.columns) {
console.warn("Data atau kolom belum tersedia untuk search index");
return;
}
if (!this.searchableColumns || this.searchableColumns.length === 0) {
// Jika searchableColumns tidak ditentukan, indeks semua kolom
this.searchableColumns = this.data.columns.reduce((acc, col) => {
if (col.columns) {
return [...acc, ...col.columns.map((subCol) => subCol.field)];
}
return [...acc, col.field];
}, []);
}
// Reset search index
this.searchIndex = {};
// Buat indeks untuk setiap kolom yang dapat dicari
this.searchableColumns.forEach((field) => {
this.searchIndex[field] = new Map();
if (!this.data || !this.data.data) {
console.warn("Data tidak tersedia untuk indexing");
return;
}
this.data.data.forEach((row, idx) => {
if (row[field] === undefined) {
console.warn(`Field "${field}" tidak ditemukan pada baris ${idx}`);
return;
}
const value = String(row[field]).toLowerCase();
if (!this.searchIndex[field].has(value)) {
this.searchIndex[field].set(value, new Set());
}
this.searchIndex[field].get(value).add(idx);
});
});
//console.log('Search index built:', this.searchIndex);
}
performSearch(searchText) {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
const startTime = performance.now();
searchText = searchText.toLowerCase();
if (searchText.length === 0) {
this.data = {
...this.originalData,
data: [...this.originalData.data],
};
this.refreshTable();
return;
}
// Gunakan filter langsung untuk pencarian sederhana
const filteredData = this.originalData.data.filter((row) => {
return this.searchableColumns.some((field) => {
const value = String(row[field] || "").toLowerCase();
return value.includes(searchText);
});
});
// Update data tabel dengan hasil pencarian
this.data = {
...this.originalData,
data: filteredData,
};
const endTime = performance.now();
//console.log(`Pencarian selesai dalam ${endTime - startTime}ms`);
this.refreshTable();
}, 300);
}
refreshTable() {
this.currentPage = 1;
if (this.virtualScrolling) {
this.setupVirtualScrolling();
} else {
this.createTable();
this.createPagination();
}
}
// Tambahkan cache untuk hasil pencarian
searchCache = new Map();
performSearchWithCache(searchText) {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
searchText = searchText.toLowerCase();
// Cek cache
if (this.searchCache.has(searchText)) {
this.data.data = this.searchCache.get(searchText);
this.refreshTable();
return;
}
// Lakukan pencarian
const results = this.performSearch(searchText);
// Simpan ke cache
if (this.searchCache.size > 100) {
// Batasi ukuran cache
const firstKey = this.searchCache.keys().next().value;
this.searchCache.delete(firstKey);
}
this.searchCache.set(searchText, results);
}, 300);
}
// Tambahkan metode untuk pencarian fuzzy
performFuzzySearch(searchText, threshold = 0.3) {
return this.originalData.data.filter((row) => {
return this.searchableColumns.some((field) => {
const value = String(row[field]).toLowerCase();
return (
this.calculateLevenshteinDistance(value, searchText.toLowerCase()) <=
threshold
);
});
});
}
calculateLevenshteinDistance(str1, str2) {
const track = Array(str2.length + 1)
.fill(null)
.map(() => Array(str1.length + 1).fill(null));
for (let i = 0; i <= str1.length; i++) track[0][i] = i;
for (let j = 0; j <= str2.length; j++) track[j][0] = j;
for (let j = 1; j <= str2.length; j++) {
for (let i = 1; i <= str1.length; i++) {
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
track[j][i] = Math.min(
track[j][i - 1] + 1,
track[j - 1][i] + 1,
track[j - 1][i - 1] + indicator
);
}
}
return track[str2.length][str1.length];
}
applyStyles(element, styles) {
Object.keys(styles).forEach((style) => {
element.style[style] = styles[style];
});
}
createPagination() {
// Cek apakah paginasi dibutuhkan
if (!this.paginationId || this.virtualScrolling) {
return;
}
const paginationContainer = document.getElementById(this.paginationId);
if (!paginationContainer) {
console.warn(
`Elemen paginasi dengan id "${this.paginationId}" tidak ditemukan, paginasi tidak akan ditampilkan.`
);
return;
}
// Hapus paginasi yang ada jika ada
paginationContainer.innerHTML = "";
// Jika tidak ada data, tidak perlu membuat paginasi
if (!this.data || !this.data.data || this.data.data.length === 0) {
return;
}
const paginationList = document.createElement("ul");
paginationList.className = `pagination justify-content-${this.paginationPosition}`;
paginationContainer.appendChild(paginationList);
this.updatePaginationInfo(paginationList);
}
updatePaginationInfo(paginationList) {
// Jika paginationList tidak diberikan, coba dapatkan dari container
if (!paginationList && this.paginationId) {
const paginationContainer = document.getElementById(this.paginationId);
if (!paginationContainer) {
return;
}
paginationList = paginationContainer.querySelector(".pagination");
if (!paginationList) {
return;
}
}
// Jika masih tidak ada paginationList, keluar
if (!paginationList) {
return;
}
// Bersihkan paginasi yang ada
paginationList.innerHTML = "";
const totalPages = Math.ceil(this.data.data.length / this.perPage);
if (totalPages <= 1) {
return; // Tidak perlu paginasi jika hanya 1 halaman
}
const createPageItem = (
text,
pageNum,
active = false,
disabled = false
) => {
const li = document.createElement("li");
li.className = `page-item ${active ? "active" : ""} ${
disabled ? "disabled" : ""
}`;
const a = document.createElement("a");
a.className = "page-link";
a.href = "#";
a.innerText = text;
if (!disabled) {
a.addEventListener("click", (e) => {
e.preventDefault();
if (text === "Previous") {
this.goToPage(this.currentPage - 1);
} else if (text === "Next") {
this.goToPage(this.currentPage + 1);
} else if (text === "Last") {
this.goToPage(totalPages);
} else if (text === "First") {
this.goToPage(1);
} else {
this.goToPage(pageNum);
}
});
}
li.appendChild(a);
return li;
};
// Tambahkan tombol First
paginationList.appendChild(
createPageItem("First", 1, false, this.currentPage === 1)
);
// Tambahkan tombol Previous
paginationList.appendChild(
createPageItem(
"Previous",
this.currentPage - 1,
false,
this.currentPage === 1
)
);
// Tambahkan nomor halaman
let startPage = Math.max(1, this.currentPage - 2);
let endPage = Math.min(startPage + 4, totalPages);
if (endPage - startPage < 4) {
startPage = Math.max(1, endPage - 4);
}
for (let i = startPage; i <= endPage; i++) {
paginationList.appendChild(createPageItem(i, i, i === this.currentPage));
}
// Tambahkan tombol Next
paginationList.appendChild(
createPageItem(
"Next",
this.currentPage + 1,
false,
this.currentPage === totalPages
)
);
// Tambahkan tombol Last
paginationList.appendChild(
createPageItem("Last", totalPages, false, this.currentPage === totalPages)
);
// Tambahkan info halaman
const pageInfo = document.createElement("li");
pageInfo.className = "page-item disabled";
pageInfo.innerHTML = `<span class="page-link">
Halaman ${this.currentPage} dari ${totalPages}
(Total: ${this.data.data.length} data)
</span>`;
paginationList.appendChild(pageInfo);
}
goToPage(pageNum) {
const totalPages = Math.ceil(this.data.data.length / this.perPage);
if (pageNum >= 1 && pageNum <= totalPages && pageNum !== this.currentPage) {
this.currentPage = pageNum;
this.createTable();
this.updatePaginationInfo();
}
}
nextPage() {
this.goToPage(this.currentPage + 1);
}
prevPage() {
this.goToPage(this.currentPage - 1);
}
setupExportButtons() {
Object.entries(this.exportOptions).forEach(([format, buttonId]) => {
const button = document.getElementById(buttonId);
if (button) {
button.textContent = format; // Set label tombol sesuai format
button.addEventListener("click", () => {
this.exportData(format);
});
} else {
console.warn(`Elemen ekspor dengan id "${buttonId}" tidak ditemukan.`);
}
});
}
async exportData(format) {
const formatKey = format.toLowerCase();
if (this.exportFormats[formatKey]) {
try {
// Load library yang diperlukan
if (formatKey === "xlsx") {
await this.loadLibrary("xlsx");
} else if (formatKey === "pdf") {
await this.loadLibrary("pdf");
await this.loadLibrary("autoTable");
}
this.exportFormats[formatKey].processor();
} catch (error) {
console.error(`Error saat export ${format}:`, error);
this.triggerExportCallback("error", formatKey, error);
}
} else {
console.warn(`Format ekspor "${format}" tidak dikenali.`);
}
}
setupExportOptions() {
this.exportFormats = {
xlsx: {
mimeType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
extension: ".xlsx",
processor: this.exportXLSX.bind(this),
},
pdf: {
mimeType: "application/pdf",
extension: ".pdf",
processor: this.exportPDF.bind(this),
},
csv: {
mimeType: "text/csv",
extension: ".csv",
processor: this.exportCSV.bind(this),
},
json: {
mimeType: "application/json",
extension: ".json",
processor: this.exportJSON.bind(this),
},
};
}
exportXLSX() {
try {
if (typeof XLSX === "undefined") {
throw new Error("Library XLSX tidak tersedia");
}
// Persiapkan data untuk export
const exportData = this.prepareExportData();
// Konfigurasi worksheet
const ws = XLSX.utils.json_to_sheet(exportData.data);
// Styling worksheet
const wsStyles = {
"!cols": exportData.columns.map(() => ({ wch: 15 })), // Auto width
"!rows": [{ hpt: 25 }], // Header height
};
// Merge dengan style yang ada
ws["!cols"] = wsStyles["!cols"];
ws["!rows"] = wsStyles["!rows"];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, this.options.sheetName || "Sheet1");
// Tambahkan metadata
wb.Props = {
Title: this.options.fileName || "Export Data",
Author: "System",
CreatedDate: new Date(),
};
XLSX.writeFile(wb, `${this.options.fileName || "export"}.xlsx`);
this.triggerExportCallback("success", "xlsx");
} catch (error) {
console.error("Error exporting XLSX:", error);
this.triggerExportCallback("error", "xlsx", error);
}
}
async exportPDF() {
try {
if (typeof window.jspdf === "undefined") {
await this.loadLibrary("pdf");
await this.loadLibrary("autoTable");
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: this.options.pdfOrientation || "landscape",
unit: "mm",
format: this.options.pdfFormat || "a4",
});
// Tambahkan header
if (this.options.title) {
doc.setFontSize(16);
doc.text(this.options.title, 14, 15);
}
// Konfigurasi autoTable
doc.autoTable({
startY: this.options.title ? 25 : 15,
head: [this.getHeaderRow()],
body: this.getDataRows(),
styles: {
fontSize: 8,
cellPadding: 2,
overflow: "linebreak",
font: "helvetica",
},
headStyles: {
fillColor: [66, 139, 202],
textColor: 255,
fontSize: 9,
fontStyle: "bold",
halign: "center",
},
columnStyles: this.getPDFColumnStyles(),
didDrawPage: (data) => {
// Footer
doc.setFontSize(8);
doc.text(
`Diekspor pada: ${new Date().toLocaleString()}`,
data.settings.margin.left,
doc.internal.pageSize.height - 10
);
},
margin: { top: 15, right: 15, bottom: 15, left: 15 },
theme: "grid",
});
// Simpan file
doc.save(`${this.options.fileName || "export"}.pdf`);
this.triggerExportCallback("success", "pdf");
} catch (error) {
console.error("Error exporting PDF:", error);
this.triggerExportCallback("error", "pdf", error);
}
}
exportCSV() {
try {
const exportData = this.prepareExportData();
const headers = this.getHeaderRow().join(",");
const rows = this.getDataRows().map((row) => row.join(","));
const csvContent = [headers, ...rows].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
this.downloadFile(blob, "csv");
this.triggerExportCallback("success", "csv");
} catch (error) {
console.error("Error exporting CSV:", error);
this.triggerExportCallback("error", "csv", error);
}
}
exportJSON() {
try {
const exportData = {
metadata: {
exportDate: new Date(),
totalRecords: this.data.data.length,
filters: this.activeFilters || {},
sorting: this.sortDirection || {},
},
data: this.data.data,
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
this.downloadFile(blob, "json");
this.triggerExportCallback("success", "json");
} catch (error) {
console.error("Error exporting JSON:", error);
this.triggerExportCallback("error", "json", error);
}
}
// Tambahkan metode untuk memuat data secara bertahap
loadDataInChunks(chunkSize = 1000) {
const totalData = this.originalData.data.length;
let loadedData = 0;
const loadChunk = () => {
const chunk = this.originalData.data.slice(
loadedData,
loadedData + chunkSize
);
this.data.data = this.data.data.concat(chunk);
loadedData += chunk.length;
this.createTable();
if (loadedData < totalData) {
setTimeout(loadChunk, 0);
}
};
loadChunk();
}
static createInstance(options) {
return new TabelMatrix(options);
}
setupVirtualScrolling() {
if (!this.virtualScrolling) return;
const container = document.getElementById(this.options.containerId);
container.innerHTML = "";
// Buat wrapper untuk virtual scrolling
const tableWrapper = document.createElement("div");
tableWrapper.className = "virtual-scroll-wrapper";
tableWrapper.style.height = `${this.options.virtualScrollHeight || 400}px`;
tableWrapper.style.overflowY = "auto";
tableWrapper.style.position = "relative";
tableWrapper.style.width = "100%";
tableWrapper.style.border = "1px solid #ddd";
tableWrapper.style.backgroundColor = "#fff";
// Buat table dengan struktur fixed
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.tableLayout = "fixed";
// Buat dan tambahkan thead
const thead = this.createTableHeader();
thead.style.position = "sticky";
thead.style.top = "0";
thead.style.zIndex = "1";
thead.style.backgroundColor = "#fff";
table.appendChild(thead);
// Buat tbody container
const tbodyContainer = document.createElement("div");
tbodyContainer.style.position = "relative";
tbodyContainer.style.width = "100%";
// Set tinggi total untuk scrolling
tbodyContainer.style.height = `${this.data.data.length * this.rowHeight}px`;
// Buat tbody untuk konten yang terlihat
const tbody = document.createElement("tbody");
tbody.style.position = "absolute";
tbody.style.width = "100%";
table.appendChild(tbody);
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
this.tableWrapper = tableWrapper;
this.tbody = tbody;
this.tbodyContainer = tbodyContainer;
this.table = table;
// Hitung jumlah baris yang terlihat
const visibleRowCount = Math.ceil(
tableWrapper.clientHeight / this.rowHeight
);
// Sesuaikan buffer size
this.bufferSize = Math.min(
Math.ceil(visibleRowCount), // Buffer 1x jumlah baris yang terlihat
50 // Maksimal 50 baris buffer
);
let lastScrollTime = 0;
const scrollThrottleMs = 16;
tableWrapper.addEventListener("scroll", () => {
const now = performance.now();
if (now - lastScrollTime >= scrollThrottleMs) {
lastScrollTime = now;
this.handleScroll(tableWrapper);
}
});
// Render awal
this.handleScroll(tableWrapper);
}
handleScroll(wrapper) {
if (!wrapper || !this.data.data.length) return;
const scrollTop = wrapper.scrollTop;
const viewportHeight = wrapper.clientHeight;
const totalHeight = this.data.data.length * this.rowHeight;
// Hitung indeks baris yang terlihat
const startIndex = Math.floor(scrollTop / this.rowHeight);
const visibleCount = Math.ceil(viewportHeight / this.rowHeight);
// Hitung range data dengan buffer
const start = Math.max(0, startIndex - this.bufferSize);
const end = Math.min(
this.data.data.length,
startIndex + visibleCount + this.bufferSize
);
// Update scroll container height jika perlu
this.tbodyContainer.style.height = `${totalHeight}px`;
// Render hanya jika range berubah
const currentRange = `${start}-${end}`;
if (this.lastRange !== currentRange) {
this.lastRange = currentRange;
this.renderRows(start, end);
}
}
renderRows(start, end) {
const fragment = document.createDocumentFragment();
for (let i = start; i < end; i++) {
const rowData = this.data.data[i];
if (!rowData) continue;
const row = document.createElement("tr");
row.style.position = "absolute";
row.style.top = `${i * this.rowHeight}px`;
row.style.width = "100%";
row.style.height = `${this.rowHeight}px`;
row.style.backgroundColor = "#ffffff";
row.style.display = "table";
row.style.tableLayout = "fixed";
// Tambahkan nomor
const numberCell = document.createElement("td");
numberCell.textContent = (i + 1).toString();
this.applyStyles(numberCell, {
textAlign: "center",
fontSize: "13px",
fontWeight: "400",
backgroundColor: "#ffffff",
padding: "10px",
border: "1px solid #ccc",
verticalAlign: "middle",
height: `${this.rowHeight}px`,
});
row.appendChild(numberCell);
// Render sel data
this.data.columns.forEach((column) => {
if (column.columns) {
column.columns.forEach((subColumn) => {
const td = this.createDataCell(rowData, subColumn, i);
row.appendChild(td);
});
} else {
const td = this.createDataCell(rowData, column, i);
row.appendChild(td);
}
});
fragment.appendChild(row);
}
// Update tbody
this.tbody.innerHTML = "";
this.tbody.appendChild(fragment);
}
createDataCell(rowData, column, rowIndex) {
const td = document.createElement("td");
const fieldToUse = column.field1 || column.field;
const editConfig = this.editableColumns[fieldToUse];
// Tambahkan class jika ada
if (column.class) {
td.className = column.class;
if (editConfig) {
td.className += " editable-cell";
}
} else if (editConfig) {
td.className = "editable-cell";
}
// Buat container untuk konten
const contentDiv = document.createElement("div");
contentDiv.style.cssText = "position: relative; min-height: 20px;";
// Simpan nilai asli
const originalValue = rowData[column.field];
// Format nilai untuk tampilan
let formattedValue = "";
if (
(editConfig && editConfig[0] === "search") ||
editConfig?.type === "search"
) {
// Untuk tipe search, ambil label dari opsi
const options = Array.isArray(editConfig)
? editConfig[5]
: editConfig.options;
// Cek apakah originalValue adalah object
const searchValue =
typeof originalValue === "object" ? originalValue.value : originalValue;
const option = options?.find((opt) => opt.value === searchValue);
formattedValue = option ? option.label : searchValue || "";
} else {
formattedValue = this.formatCellValue(originalValue, column);
}
contentDiv.innerHTML = formattedValue || "";
// Tambahkan ikon edit jika sel dapat diedit
if (editConfig) {
const iconSpan = document.createElement("span");
iconSpan.className = "edit-icon";
iconSpan.innerHTML =
'<i data-feather="edit-2" style="width: 14px; height: 14px;"></i>';
td.appendChild(iconSpan);
if (typeof feather !== "undefined") {
feather.replace();
}
// Event listener untuk edit
td.addEventListener("click", () => {
if (this.activeEditor) return;
// Cek kondisional editing jika ada
if (editConfig.conditional && editConfig.conditional.editable) {
if (!editConfig.conditional.editable(rowData)) {
return; // Tidak dapat diedit berdasarkan kondisi
}
}
const editor = this.createEditor(
editConfig.type || editConfig[0],
editConfig,
originalValue,
(newValue) => {
if (newValue !== null) {
// Simpan nilai baru
rowData[column.field] = newValue;
// Format dan tampilkan nilai baru
const newFormattedValue = this.formatCellValue(newValue, column);
contentDiv.innerHTML = newFormattedValue;
if (this.options.onCellEdit) {
this.options.onCellEdit(
rowData,
column.field,
newValue,
rowIndex
);
}
}
editor.remove();
contentDiv.style.display = "block";
iconSpan.style.display = "block";
}
);
contentDiv.style.display = "none";
iconSpan.style.display = "none";
td.appendChild(editor);
editor.focus();
this.activeEditor = editor;
});
}
td.appendChild(contentDiv);
this.applyStyles(td, {
fontSize: "13px",
fontWeight: "400",
backgroundColor: "#ffffff",
padding: "10px",
border: "1px solid #ccc",
textAlign: typeof rowData[column.field] === "number" ? "right" : "left",
verticalAlign: "top",
});
// Tambahkan CSS classes untuk alignment
if (td.className.includes("center")) {
td.style.textAlign = "center";
} else if (td.className.includes("right")) {
td.style.textAlign = "right";
} else if (td.className.includes("left")) {
td.style.textAlign = "left";
}
// Tambahkan CSS class untuk bold
if (td.className.includes("bold")) {
td.style.fontWeight = "bold";
}
return td;
}
// Tambahkan juga method formatCellValue jika belum ada
formatCellValue(value, column) {
if (value === null || value === undefined) return "";
const fieldToUse = column.field1 || column.field;
// Jika ada formatter dan nilai bukan HTML
if (this.columnFormatters[fieldToUse] && !this.isHTML(value)) {
return this.columnFormatters[fieldToUse](value);
}
// Jika nilai sudah dalam format HTML atau tidak ada formatter
return value;
}
// Tambahkan method isHTML jika belum ada
isHTML(str) {
if (typeof str !== "string") return false;
return str.trim().startsWith("<") && str.trim().endsWith(">");
}
// Tambahkan method formatDate ke dalam class TabelMatrix
formatDate(date, format) {
const pad = (num) => String(num).padStart(2, "0");
const formatMap = {
Y: date.getFullYear(),
y: date.getFullYear().toString().slice(-2),
m: pad(date.getMonth() + 1),
d: pad(date.getDate()),
H: pad(date.getHours()),
i: pad(date.getMinutes()),
s: pad(date.getSeconds()),
j: date.getDate(),
n: date.getMonth() + 1,
F: new Intl.DateTimeFormat("id-ID", { month: "long" }).format(date),
M: new Intl.DateTimeFormat("id-ID", { month: "short" }).format(date),
l: new Intl.DateTimeFormat("id-ID", { weekday: "long" }).format(date),
D: new Intl.DateTimeFormat("id-ID", { weekday: "short" }).format(date),
};
return format
.split("")
.map((char) => formatMap[char] || char)
.join("");
}
// Tambahkan juga method untuk parsing tanggal
parseDate(dateString, format) {
// Jika dateString sudah dalam format ISO atau timestamp
if (!isNaN(Date.parse(dateString))) {
return new Date(dateString);
}
// Jika format custom
const formatParts = format.split(/[^YymdHis]/);
const dateParts = dateString.split(/[^0-9]/);
const formatMap = {};
format.split("").forEach((char, index) => {
if ("YymdHis".includes(char)) {
formatMap[char] =
dateParts[formatParts.findIndex((part) => part.includes(char))];
}
});
const year = formatMap["Y"] || formatMap["y"] || new Date().getFullYear();
const month = (formatMap["m"] || 1) - 1;
const day = formatMap["d"] || 1;
const hours = formatMap["H"] || 0;
const minutes = formatMap["i"] || 0;
const seconds = formatMap["s"] || 0;
return new Date(year, month, day, hours, minutes, seconds);
}
// Tambahkan method Reload
Reload(options) {
// Validasi parameter
if (!options || (!options.row && !options.data)) {
console.warn(
"Parameter reload tidak valid. Gunakan format: { row: dataset } atau { data: { columns: [], data: [] }}"
);
return;
}
try {
// Update data berdasarkan parameter yang diberikan
if (options.row) {
// Jika hanya update data rows
this.data.data = [...options.row];
this.originalData.data = [...options.row];
} else if (options.data) {
// Jika update keseluruhan struktur data
this.data = {
columns: [...(options.data.columns || this.data.columns)],
data: [...options.data.data],
};
this.originalData = {
columns: [...(options.data.columns || this.data.columns)],
data: [...options.data.data],
};
}
// Reset state
this.currentPage = 1;
this.sortDirection = {};
// Rebuild search index
this.buildSearchIndex();
// Reset filters jika ada
if (this.filters) {
Object.keys(this.filters).forEach((filterKey) => {
const filterConfig = this.filters[filterKey];
const filterElement = document.getElementById(filterConfig.element);
if (filterElement) {
if (filterConfig.type === "select") {
this.populateFilterOptions(filterElement, filterConfig.field);
}
filterElement.value = "all";
}
});
}
// Perbarui tampilan
if (this.virtualScrolling) {
this.setupVirtualScrolling();
} else {
this.createTable();
this.createPagination();
}
//console.log('Tabel berhasil dimuat ulang');
return true;
} catch (error) {
console.error("Gagal memuat ulang tabel:", error);
return false;
}
}
// Tambahkan method addTabel
addTabel(newData) {
try {
// Validasi parameter
if (!newData || !newData.columns) {
throw new Error(
"Data tidak valid. Format yang benar: { columns: [...] }"
);
}
// Buat data row baru dari values di kolom
const newRow = {};
// Map nilai-nilai dari kolom baru ke struktur kolom yang ada
newData.columns.forEach((column) => {
if (column.columns) {
column.columns.forEach((subCol) => {
// Gunakan field dari struktur kolom yang ada jika ada
const existingSubCol = this.originalData.columns
.find((c) => c.columns)
?.columns.find((sc) => sc.title === subCol.title);
const field =
existingSubCol?.field ||
subCol.field ||
`field_${Math.random().toString(36).substr(2, 9)}`;
if (subCol.value !== undefined) {
newRow[field] = subCol.value;
}
});
} else {
// Untuk kolom tunggal
const existingCol = this.originalData.columns.find(
(c) => c.title === column.title
);
const field = existingCol?.field || column.field || column.value;
if (column.value !== undefined) {
newRow[field] = column.value;
}
}
});
// Gabungkan data baru dengan data yang ada
this.data = {
// Gunakan struktur kolom yang ada
columns: [...this.originalData.columns],
// Tambahkan data baru di awal array
data: [newRow, ...this.originalData.data],
};
// Update originalData juga
this.originalData = {
columns: [...this.originalData.columns],
data: [newRow, ...this.originalData.data],
};
// Reset state
this.currentPage = 1;
this.sortDirection = {};
// Rebuild search index
this.buildSearchIndex();
// Perbarui tampilan
if (this.virtualScrolling) {
this.setupVirtualScrolling();
} else {
this.createTable();
this.createPagination();
}
//console.log('Data baru berhasil ditambahkan di urutan pertama');
return true;
} catch (error) {
console.error("Gagal menambahkan data:", error);
return false;
}
}
// Tambahkan method filterKey ke dalam class TabelMatrix
filterKey(key, value) {
try {
// Validasi parameter
if (!key || value === undefined) {
throw new Error(
"Parameter tidak valid. Gunakan format: filterKey(key, value)"
);
}
// Simpan data yang difilter
const filteredData = this.originalData.data.filter((row) => {
// Konversi nilai ke string untuk perbandingan yang konsisten
const rowValue = String(row[key] || "").toLowerCase();
const searchValue = String(value).toLowerCase();
// Gunakan includes untuk pencocokan parsial
return rowValue.includes(searchValue);
});
// Update data tabel dengan hasil filter
this.data = {
columns: [...this.originalData.columns],
data: filteredData,
};
// Reset state
this.currentPage = 1;
this.sortDirection = {};
// Rebuild search index
this.buildSearchIndex();
// Perbarui tampilan
if (this.virtualScrolling) {
this.setupVirtualScrolling();
} else {
this.createTable();
this.createPagination();
}
//console.log(`Data berhasil difilter berdasarkan ${key}=${value}`);
return true;
} catch (error) {
console.error("Gagal memfilter data:", error);
return false;
}
}
// Tambahkan method untuk mereset filter
resetFilter() {
// Kembalikan ke data original
this.data = {
columns: [...this.originalData.columns],
data: [...this.originalData.data],
};
// Reset state
this.currentPage = 1;
this.sortDirection = {};
// Rebuild search index
this.buildSearchIndex();
// Perbarui tampilan
if (this.virtualScrolling) {
this.setupVirtualScrolling();
} else {
this.createTable();
this.createPagination();
}
//console.log('Filter direset');
}
// Method baru untuk menangani pembuatan editor
createEditor(type, config, value, callback) {
if (type === "search") {
const editor = document.createElement("select");
// Handle konfigurasi array format
let width, placeholder, searchable, options;
if (Array.isArray(config)) {
[, width, , placeholder, searchable, options] = config;
} else {
width = config.width || 6;
placeholder = config.placeholder;
searchable = config.searchable;
options = config.options;
}
editor.className = `form-control col-md-${width}`;
editor.innerHTML = `<option value="">${
placeholder || "Pilih..."
}</option>`;
// Handle options
const optionsList = Array.isArray(options)
? options
: typeof options === "function"
? options(this.currentRow)
: [];
// Tambahkan semua opsi ke select dan cari label yang sesuai dengan value
let selectedLabel = "";
optionsList.forEach((opt) => {
const option = document.createElement("option");
option.value = opt.value;
option.textContent = opt.label;
// Simpan label jika value cocok
if (value === opt.value) {
option.selected = true;
selectedLabel = opt.label;
}
editor.appendChild(option);
});
// Tambahkan fitur searchable
if (searchable) {
if (window.jQuery && window.jQuery.fn.select2) {
jQuery(editor).select2({
placeholder: placeholder || "Pilih...",
allowClear: true,
width: "100%",
dropdownParent: jQuery(editor).parent(),
// Inisialisasi dengan label yang sesuai
initSelection: function (element, callback) {
if (value && selectedLabel) {
callback({ id: value, text: selectedLabel });
}
},
});
// Set nilai awal untuk select2
if (value && selectedLabel) {
const newOption = new Option(selectedLabel, value, true, true);
jQuery(editor).append(newOption).trigger("change");
}
}
}
// Event handlers
editor.addEventListener("change", () => {
const newValue = editor.value;
const selectedOption = optionsList.find(
(opt) => opt.value === newValue
);
if (newValue && selectedOption) {
// Hanya simpan value, bukan object
callback(newValue);
} else {
callback(null);
}
this.activeEditor = null;
});
editor.addEventListener("blur", () => {
setTimeout(() => {
if (!this.activeEditor) return;
const newValue = editor.value;
const selectedOption = optionsList.find(
(opt) => opt.value === newValue
);
if (newValue && selectedOption) {
// Hanya simpan value, bukan object
callback(newValue);
} else {
callback(null);
}
this.activeEditor = null;
}, 100);
});
editor.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
this.activeEditor = null;
callback(null);
}
});
return editor;
}
// Lanjutkan dengan kode editor yang sudah ada
const cleanValue = this.cleanValueForEditor(value);
const editor = document.createElement(
type === "select" ? "select" : "input"
);
// Dapatkan konfigurasi yang diperluas
const editorConfig = this.getEditorConfig(type, config, cleanValue);
switch (type) {
case "text":
case "number":
editor.type = type;
editor.className = `form-control col-md-${editorConfig.width}`;
editor.placeholder = editorConfig.placeholder;
editor.value = cleanValue;
// Tambahkan validasi HTML5
if (editorConfig.validation) {
if (editorConfig.validation.required) editor.required = true;
if (editorConfig.validation.minLength)
editor.minLength = editorConfig.validation.minLength;
if (editorConfig.validation.maxLength)
editor.maxLength = editorConfig.validation.maxLength;
if (editorConfig.validation.pattern)
editor.pattern = editorConfig.validation.pattern;
if (type === "number") {
if (editorConfig.validation.min)
editor.min = editorConfig.validation.min;
if (editorConfig.validation.max)
editor.max = editorConfig.validation.max;
if (editorConfig.validation.step)
editor.step = editorConfig.validation.step;
}
}
break;
case "select":
editor.className = `form-control col-md-${editorConfig.width}`;
editor.innerHTML = `<option value="">${editorConfig.placeholder}</option>`;
// Handle dynamic options
const options =
typeof editorConfig.options === "function"
? editorConfig.options(this.currentRow)
: editorConfig.options;
options.forEach((opt) => {
const option = document.createElement("option");
option.value = opt.value;
option.textContent = opt.label;
option.selected = cleanValue === opt.value;
editor.appendChild(option);
});
// Add searchable feature if enabled
if (editorConfig.searchable) {
this.makeSelectSearchable(editor);
}
break;
case "datepicker":
editor.type = "date";
editor.className = `form-control col-md-${editorConfig.width}`;
editor.placeholder = editorConfig.placeholder;
// Handle date constraints
if (editorConfig.minDate) editor.min = editorConfig.minDate;
if (editorConfig.maxDate) editor.max = editorConfig.maxDate;
// Format tanggal untuk input
if (cleanValue) {
const date = new Date(cleanValue);
editor.value = date.toISOString().split("T")[0];
}
// Tambahkan event handler khusus untuk datepicker
editor.addEventListener("change", () => {
if (!editor.value) {
callback(null);
return;
}
const date = new Date(editor.value);
let formattedDate;
// Format sesuai dengan placeholder yang ditentukan
const format = editorConfig.placeholder;
if (format) {
formattedDate = this.formatDate(date, format);
} else {
formattedDate = date.toISOString().split("T")[0];
}
callback(formattedDate);
this.activeEditor = null;
});
break;
case "textarea":
const textarea = document.createElement("textarea");
textarea.className = `form-control col-md-${editorConfig.width}`;
textarea.placeholder = editorConfig.placeholder;
textarea.value = cleanValue;
textarea.rows = editorConfig.rows || 3;
if (editorConfig.maxLength) textarea.maxLength = editorConfig.maxLength;
return textarea;
case "checkbox":
case "radio":
const wrapper = document.createElement("div");
wrapper.className = `col-md-${editorConfig.width}`;
editorConfig.options.forEach((opt) => {
const container = document.createElement("div");
container.className = `form-check`;
const input = document.createElement("input");
input.type = type;
input.className = "form-check-input";
input.name = editorConfig.name || "group";
input.value = opt.value;
input.checked = cleanValue === opt.value;
const label = document.createElement("label");
label.className = "form-check-label";
label.textContent = opt.label;
container.appendChild(input);
container.appendChild(label);
wrapper.appendChild(container);
});
return wrapper;
}
// Tambahkan event handlers
if (editorConfig.events) {
Object.entries(editorConfig.events).forEach(([event, handler]) => {
editor.addEventListener(event, handler);
});
}
// Event handlers default
editor.addEventListener("blur", () => {
setTimeout(() => {
if (!this.activeEditor) return;
let newValue = editor.value;
// Validasi
if (
editorConfig.validation &&
!this.validateValue(newValue, editorConfig.validation)
) {
editor.classList.add("is-invalid");
return;
}
// Transform nilai setelah edit
if (editorConfig.transform && editorConfig.transform.afterEdit) {
newValue = editorConfig.transform.afterEdit(newValue);
}
this.activeEditor = null;
callback(newValue);
}, 100);
});
editor.addEventListener("keydown", (e) => {
if (e.key === "Enter" && type !== "textarea") {
e.preventDefault();
let newValue = editor.value;
if (
editorConfig.validation &&
!this.validateValue(newValue, editorConfig.validation)
) {
editor.classList.add("is-invalid");
return;
}
if (editorConfig.transform && editorConfig.transform.afterEdit) {
newValue = editorConfig.transform.afterEdit(newValue);
}
this.activeEditor = null;
callback(newValue);
}
if (e.key === "Escape") {
e.preventDefault();
this.activeEditor = null;
callback(null);
}
});
return editor;
}
// Method baru untuk mendapatkan konfigurasi editor yang diperluas
getEditorConfig(type, config, value) {
// Jika config adalah array (format lama)
if (Array.isArray(config)) {
return {
type: type,
width: config[1],
label: config[2],
placeholder: config[3],
options: config[4] || [],
};
}
// Format baru (object)
return {
type: type,
width: config.width || 12,
label: config.label || "",
placeholder: config.placeholder || "",
validation: config.validation || {},
transform: config.transform || {},
events: config.events || {},
options: config.options || [],
searchable: config.searchable || false,
multiple: config.multiple || false,
rows: config.rows,
maxLength: config.maxLength,
minDate: config.minDate,
maxDate: config.maxDate,
format: config.format,
conditional: config.conditional || {},
};
}
// Method untuk validasi nilai
validateValue(value, validation) {
if (!validation) return true;
if (validation.required && !value) return false;
if (validation.minLength && value.length < validation.minLength)
return false;
if (validation.maxLength && value.length > validation.maxLength)
return false;
if (validation.pattern && !validation.pattern.test(value)) return false;
if (validation.custom && !validation.custom(value)) return false;
return true;
}
// Method untuk membuat select searchable
makeSelectSearchable(select) {
// Implementasi fitur pencarian untuk select
// Bisa menggunakan library seperti select2 atau implementasi custom
}
// Method untuk membersihkan HTML untuk editor
cleanValueForEditor(value) {
if (!value) return "";
// Jika nilai bukan string, konversi ke string
if (typeof value !== "string") {
return String(value);
}
// Jika nilai adalah HTML
if (this.isHTML(value)) {
const temp = document.createElement("div");
temp.innerHTML = value;
const cleanText = temp.textContent || temp.innerText || "";
temp.remove();
return cleanText;
}
// Jika nilai bukan HTML, kembalikan apa adanya
return value;
}
// Tambahkan method helper untuk mendapatkan label dari nilai search
getSearchLabel(value, config) {
if (!value) return "";
const options = config.options || config[4] || [];
const option = options.find((opt) => opt.value === value);
return option ? option.label : value;
}
// Tambahkan method setupFilters
setupFilters() {
if (!this.filters) return;
// Iterasi semua filter yang didefinisikan
Object.keys(this.filters).forEach((filterKey) => {
const filterConfig = this.filters[filterKey];
const filterElement = document.getElementById(filterConfig.element);
if (filterElement) {
// Jika tipe select, populate options
if (filterConfig.type === "select") {
this.populateFilterOptions(filterElement, filterConfig.field);
}
// Tambahkan event listener
filterElement.addEventListener("change", () => {
this.filterData();
});
}
});
}
// Tambahkan method populateFilterOptions
populateFilterOptions(selectElement, field) {
// Dapatkan nilai unik untuk field
const uniqueValues = [
...new Set(
this.originalData.data.map((item) => item[field]).filter(Boolean)
),
] // Remove null/undefined
.sort();
// Tambahkan options ke select element
selectElement.innerHTML = `
<option value="all">Semua</option>
${uniqueValues
.map((value) => `<option value="${value}">${value}</option>`)
.join("")}
`;
}
// Tambahkan method filterData
filterData() {
// Mulai dengan semua data original
let filteredData = [...this.originalData.data];
// Terapkan setiap filter secara berurutan
Object.keys(this.filters).forEach((filterKey) => {
const filterConfig = this.filters[filterKey];
const filterElement = document.getElementById(filterConfig.element);
if (
filterElement &&
filterElement.value &&
filterElement.value !== "all"
) {
filteredData = filteredData.filter((row) => {
const filterField = filterConfig.field;
const rowValue = row[filterField];
// Handle different filter types
switch (filterConfig.type) {
case "select":
return String(rowValue) === String(filterElement.value);
case "date":
if (!rowValue || !filterElement.value) return false;
const rowDate = new Date(rowValue);
const filterDate = new Date(filterElement.value);
return rowDate.toDateString() === filterDate.toDateString();
default:
return String(rowValue) === String(filterElement.value);
}
});
}
});
// Update data tabel dengan hasil filter
this.data = {
columns: [...this.originalData.columns],
data: filteredData,
};
// Reset state dan perbarui tampilan
this.currentPage = 1;
this.createTable();
this.createPagination();
this.buildSearchIndex();
}
}
//AND Tabel Matrix
// DarkMode
// AND DarkMode
export class Crypto {
constructor(tokenize) {
this.init = new Ngorei();
this.Net = new Ngorei().Network();
}
async authenticate(data, callbacks = {}) {
try {
// 1. Generate token
const encrypt = this.init.Tokenize(
{
payload: data.payload,
},
data.endpoint
);
// Callback untuk hasil token
if (callbacks.onToken) {
callbacks.onToken(encrypt);
}
// 2. Kirim request
const response = await this.Net.Brief({
endpoint: data.endpoint,
token: encrypt,
});
// Callback untuk status permintaan
if (callbacks.onStatus) {
callbacks.onStatus(response);
}
// 3. Decrypt response
const decrypt = this.init.Tokenize(
{
payload: response.data.token,
},
data.endpoint
);
// Callback untuk hasil akhir
if (callbacks.onResult) {
callbacks.onResult(decrypt);
}
return decrypt;
} catch (error) {
// console.error('Authentication error:', error);
// throw error;
}
}
}
export function onCookie(name, value) {
// Membuat cookie dengan waktu kedaluwarsa sesi
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(
value
)}; path=/`;
}
export function getOnCookie(name) {
const cookies = document.cookie.split("; ");
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split("=");
if (decodeURIComponent(cookieName) === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}
export class getViewStack {
getViewInfo() {
const currentUrl = window.location.hash.substring(1);
const activeView = document.querySelector("[data-stack][data-active]");
return {
URL: currentUrl || "home",
View: activeView?.id || "home",
Page: currentUrl ? currentUrl.split("/")[0] : "home",
Params: currentUrl ? currentUrl.split("/").slice(1).join(", ") : "",
};
}
}
const TOKEN_CONFIG = {
expiresIn: 60 * 60 * 24 * 7, // 7 hari dalam detik
algorithm: "AES-CBC",
keySize: 256,
};
export function encryptToken(data, cradensial) {
const tokenData = {
...data,
exp: Math.floor(Date.now() / 1000) + TOKEN_CONFIG.expiresIn,
iat: Math.floor(Date.now() / 1000),
};
try {
const jsonString = JSON.stringify(tokenData);
const keyBytes = CryptoJS.SHA256(cradensial);
const iv = CryptoJS.lib.WordArray.random(16);
const encrypted = CryptoJS.AES.encrypt(jsonString, keyBytes, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const ivCiphertext = iv.concat(encrypted.ciphertext);
return encodeURIComponent(CryptoJS.enc.Base64.stringify(ivCiphertext));
} catch (error) {
throw new Error("Gagal mengenkripsi data: " + error.message);
}
}
export function decryptToken(token, cradensial) {
try {
const decodedToken = decodeURIComponent(token);
const cipherData = CryptoJS.enc.Base64.parse(decodedToken);
const keyBytes = CryptoJS.SHA256(cradensial);
const iv = CryptoJS.lib.WordArray.create(cipherData.words.slice(0, 4));
const ciphertext = CryptoJS.lib.WordArray.create(cipherData.words.slice(4));
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
keyBytes,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}
);
const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedString) {
throw new Error("Hasil dekripsi kosong");
}
const decoded = JSON.parse(decryptedString);
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
throw new Error("Token sudah kadaluarsa");
}
return decoded;
} catch (error) {
throw new Error("Token tidak valid: " + error.message);
}
}
export function tokenize(userData, cradensial) {
if (!userData?.payload) {
throw new Error("Data tidak lengkap");
}
const { payload } = userData;
if (typeof payload === "string") {
return decryptToken(payload, cradensial);
}
if (
Array.isArray(payload) ||
(typeof payload === "object" && payload !== null)
) {
return encryptToken(userData, cradensial);
}
throw new Error("Format payload tidak valid");
}
// stylesheet
export function stylesheet(sy) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "./" + sy;
document.head.appendChild(link);
}