diff --git a/dist/rpc-table.css b/dist/rpc-table.css new file mode 100644 index 0000000..c6b8c1b --- /dev/null +++ b/dist/rpc-table.css @@ -0,0 +1,40 @@ +.rpc .rpc-hidden { + display: none; +} + +.rpc tbody > tr.has-child > .rpc-toggler::before { + content: ""; + margin-right: 0.5em; + display: inline-block; + box-sizing: border-box; + content: ""; + border-top: 5px solid transparent; + border-left: 10px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid transparent; + border-right: 0px solid transparent; +} + +.rpc tbody > tr.has-child.rpc-expanded > .rpc-toggler::before { + border-top: 10px solid rgba(0, 0, 0, 0.5); + border-left: 5px solid transparent; + border-bottom: 0px solid transparent; + border-right: 5px solid transparent; +} + +.rpc tbody > tr.child ul { + display: inline-block; + list-style-type: none; + margin: 0; + padding: 0; +} + +.rpc tbody > tr.child ul li { + border-bottom: 1px solid #efefef; + padding: 0.5em 0; +} + +.rpc tbody > tr.child span.rpc-child-title { + display: inline-block; + min-width: 75px; + font-weight: bold; +} \ No newline at end of file diff --git a/dist/rpc-table.js b/dist/rpc-table.js new file mode 100644 index 0000000..21cd161 --- /dev/null +++ b/dist/rpc-table.js @@ -0,0 +1,258 @@ +class RpcTable { + /** @type {HTMLTableElement} */ + #table; + + /** @type {Array} */ + #children; + + /** @type {Object} */ + #breakpoints; + + /** + * Retrieves the headers of the table. + * @returns {Array} An array containing the innerText of each th element in the first row of the table. + */ + get tableHeaders() { + return [...this.#table.querySelectorAll("tr:nth-child(1) th")] + .map((node) => node.innerText); + } + + /** @type {number} */ + #resizeTimeout; + + /** + * @param {string} selector CSS selector + * @param {{breakpoints: Object, resizeTimeout: number}} options + */ + constructor(selector, options = {}) { + this.#table = document.querySelector(selector); + if (!this.#table) { + throw new Error("Invalid selector") + } + this.#table.classList.add("rpc"); + + this.#children = []; + this.#breakpoints = options.breakpoints || { + "collapse-xs": 576, + "collapse-sm": 768, + "collapse-md": 992, + "collapse-lg": 1200, + "collapse": Number.MAX_SAFE_INTEGER, + } + this.#resizeTimeout = options.resizeTimeout || 150; + + this.process(); + + this.#table.addEventListener("click", (event) => { + if (!event.target.classList.contains("rpc-toggler")) + return + + let tr = event.target.closest("tr"); + if (tr.classList.contains("rpc-expanded")) { + tr.nextSibling.remove() + } else { + tr.after(this.#children[tr.dataset.childIndex]) + } + + tr.classList.toggle("rpc-expanded") + }) + window.addEventListener("resize", this.#handleResize.bind(this)) + } + + /** + * Processes the table to create child rows. + * + * Creates child rows for each row in the table body that does not have the "child" class. + * Assigns a unique index to each child row. + * Renders the updated table. + */ + process() { + this.#children = []; + + this.#table.querySelectorAll("tbody tr:not(.child)").forEach((tr) => { + let child = Object.assign(document.createElement("tr"), { + classList: ["child"], + colspan: "100%", + innerHTML: `
    `, + }); + + + tr.querySelectorAll("td").forEach((td, i) => { + if (!td.classList.contains("rpc-hidden")) + return + child.querySelector("td > ul").appendChild(this.#createChildLi(td)) + }); + + tr.dataset.childIndex = this.#children.length; + tr.dataset.row = tr.rowIndex; + + this.#children.push(child); + }) + this.render(); + } + + /** + * Renders the table with updated child rows and responsive classes. + * + * Toggles the responsive class based on the visibility of child rows. + * Updates the child rows based on the visibility of cells in each row. + * Removes child rows that are no longer needed. + * Updates the toggler class for rows with child rows. + */ + render() { + this.#toggleResponsiveClass(); + + this.#table.querySelectorAll("tbody tr:not(.child)").forEach((tr) => { + let child = this.#children[tr.dataset.childIndex]; + /** @type {HTMLUListElement} */ + let childContainer = child.querySelector("tr > td > ul"); + + for (let td of tr.cells) { + if (td.classList.contains("rpc-hidden") + && !this.#inChild(child, tr.dataset.row, td.cellIndex) + ) { + // create child + let index = childContainer.children.length; + while (true) { + if (index == 0) { + childContainer.prepend(this.#createChildLi(td)); + break; + } + + index--; + if (td.cellIndex > childContainer.children[index].dataset.column) { + childContainer.children[index].after(this.#createChildLi(td)); + break; + } + } + continue; + } + + if (!td.classList.contains("rpc-hidden") + && this.#inChild(child, tr.dataset.row, td.cellIndex) + ) { + // remove child + for (let li of childContainer.getElementsByTagName("li")) { + if (li.dataset.column != td.cellIndex) { + continue; + } + + for (let row of this.#table.rows) { + if (row.dataset.row != tr.dataset.row) + continue; + + let span = li.querySelector(".rpc-child-value"); + if (span.children.length > 0) { + this.#table.rows[row.rowIndex].cells[parseInt(li.dataset.column)].append(...span.children); + } else { + this.#table.rows[row.rowIndex].cells[parseInt(li.dataset.column)].innerHTML = span.innerHTML; + } + li.remove(); + } + break; + } + continue; + } + } + + tr.querySelector('td.rpc-toggler')?.classList.remove("rpc-toggler"); + if (childContainer.children.length > 0) { + tr.classList.add("has-child") + tr.querySelector('td:not(.rpc-hidden)').classList.add("rpc-toggler"); + } else { + tr.classList.remove("has-child") + if (tr.classList.contains("rpc-expanded")) { + this.#table.rows[tr.rowIndex + 1].remove() + tr.classList.remove("rpc-expanded") + } + } + }); + } + + /** + * Retrieves the hidden classes based on the current window width and breakpoints. + * @returns {Array} An array of class names that are hidden based on the current window width. + */ + hiddenClasses() { + return Object.keys(this.#breakpoints) + .filter((k) => this.#breakpoints[k] >= window.innerWidth) + } + + /** + * Toggles the responsive class for table headers and cells based on the current window width and breakpoints. + */ + #toggleResponsiveClass() { + this.#table.querySelectorAll("thead > tr > th").forEach((th, i) => { + let responsive = [...th.classList].filter(c => Object.keys(this.#breakpoints).includes)[0] + + this.#table.querySelectorAll(`tbody tr:not(.child) td:nth-child(${i + 1})`).forEach((td) => { + if (responsive) + td.classList.add(responsive) + + if (responsive && this.hiddenClasses().includes(responsive)) { + th.classList.add("rpc-hidden") + td.classList.add("rpc-hidden") + } else { + th.classList.remove("rpc-hidden") + td.classList.remove("rpc-hidden") + } + }); + }); + } + + /** + * Checks if a cell is present in a child row. + * + * @param {HTMLElement} child - The child element. + * @param {number} row - The row value of the cell. + * @param {number} column - The column value of the cell. + * @returns {boolean} True if the cell is present in the child row, false otherwise. + */ + #inChild(child, row, column) { + let ul = child.querySelector("ul"); + for (let li of ul.getElementsByTagName("li")) { + if (ul.dataset.row == row && li.dataset.column == column) { + return true + } + } + return false + } + + /** + * Creates a child list item element for a given table cell. + * + * @param {HTMLTableCellElement} td - The table cell element. + * @returns {HTMLLIElement} The created list item element. + */ + #createChildLi(td) { + let li = document.createElement("li"); + li.dataset.column = td.cellIndex; + + li.appendChild(Object.assign(document.createElement("span"), { + classList: ["rpc-child-title"], + innerHTML: this.tableHeaders[td.cellIndex], + })); + + li.appendChild(Object.assign(document.createElement("span"), { + classList: ["rpc-child-value"], + })); + + if (td.children.length > 0) { + li.children[1].append(...td.children) + } else { + li.children[1].innerHTML = td.innerText + } + + return li + } + + /** + * Handle screen resize. + * + * @param {UIEvent} event + */ + #handleResize(event) { + clearTimeout(this.#resizeTimeout); + this.#resizeTimeout = setTimeout(this.render.bind(this), this.#resizeTimeout); + } +} \ No newline at end of file diff --git a/dist/rpc-table.min.css b/dist/rpc-table.min.css new file mode 100644 index 0000000..e60b440 --- /dev/null +++ b/dist/rpc-table.min.css @@ -0,0 +1 @@ +.rpc .rpc-hidden{display:none}.rpc tbody>tr.has-child>.rpc-toggler::before{content:"";margin-right:.5em;display:inline-block;box-sizing:border-box;content:"";border-top:5px solid transparent;border-left:10px solid rgba(0,0,0,.5);border-bottom:5px solid transparent;border-right:0px solid transparent}.rpc tbody>tr.has-child.rpc-expanded>.rpc-toggler::before{border-top:10px solid rgba(0,0,0,.5);border-left:5px solid transparent;border-bottom:0 solid transparent;border-right:5px solid transparent}.rpc tbody>tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}.rpc tbody>tr.child ul li{border-bottom:1px solid #efefef;padding:.5em 0}.rpc tbody>tr.child span.rpc-child-title{display:inline-block;min-width:75px;font-weight:700} \ No newline at end of file diff --git a/dist/rpc-table.min.js b/dist/rpc-table.min.js new file mode 100644 index 0000000..fc9d21e --- /dev/null +++ b/dist/rpc-table.min.js @@ -0,0 +1 @@ +class RpcTable{#e;#t;#s;get tableHeaders(){return[...this.#e.querySelectorAll("tr:nth-child(1) th")].map((e=>e.innerText))}#l;constructor(e,t={}){if(this.#e=document.querySelector(e),!this.#e)throw new Error("Invalid selector");this.#e.classList.add("rpc"),this.#t=[],this.#s=t.breakpoints||{"collapse-xs":576,"collapse-sm":768,"collapse-md":992,"collapse-lg":1200,collapse:Number.MAX_SAFE_INTEGER},this.#l=t.resizeTimeout||150,this.process(),this.#e.addEventListener("click",(e=>{if(!e.target.classList.contains("rpc-toggler"))return;let t=e.target.closest("tr");t.classList.contains("rpc-expanded")?t.nextSibling.remove():t.after(this.#t[t.dataset.childIndex]),t.classList.toggle("rpc-expanded")})),window.addEventListener("resize",this.#i.bind(this))}process(){this.#t=[],this.#e.querySelectorAll("tbody tr:not(.child)").forEach((e=>{let t=Object.assign(document.createElement("tr"),{classList:["child"],colspan:"100%",innerHTML:`
      `});e.querySelectorAll("td").forEach(((e,s)=>{e.classList.contains("rpc-hidden")&&t.querySelector("td > ul").appendChild(this.#r(e))})),e.dataset.childIndex=this.#t.length,e.dataset.row=e.rowIndex,this.#t.push(t)})),this.render()}render(){this.#a(),this.#e.querySelectorAll("tbody tr:not(.child)").forEach((e=>{let t=this.#t[e.dataset.childIndex],s=t.querySelector("tr > td > ul");for(let l of e.cells)if(!l.classList.contains("rpc-hidden")||this.#n(t,e.dataset.row,l.cellIndex)){if(l.classList.contains("rpc-hidden")||!this.#n(t,e.dataset.row,l.cellIndex));else for(let t of s.getElementsByTagName("li"))if(t.dataset.column==l.cellIndex){for(let s of this.#e.rows){if(s.dataset.row!=e.dataset.row)continue;let l=t.querySelector(".rpc-child-value");l.children.length>0?this.#e.rows[s.rowIndex].cells[parseInt(t.dataset.column)].append(...l.children):this.#e.rows[s.rowIndex].cells[parseInt(t.dataset.column)].innerHTML=l.innerHTML,t.remove()}break}}else{let e=s.children.length;for(;;){if(0==e){s.prepend(this.#r(l));break}if(e--,l.cellIndex>s.children[e].dataset.column){s.children[e].after(this.#r(l));break}}}e.querySelector("td.rpc-toggler")?.classList.remove("rpc-toggler"),s.children.length>0?(e.classList.add("has-child"),e.querySelector("td:not(.rpc-hidden)").classList.add("rpc-toggler")):(e.classList.remove("has-child"),e.classList.contains("rpc-expanded")&&(this.#e.rows[e.rowIndex+1].remove(),e.classList.remove("rpc-expanded")))}))}hiddenClasses(){return Object.keys(this.#s).filter((e=>this.#s[e]>=window.innerWidth))}#a(){this.#e.querySelectorAll("thead > tr > th").forEach(((e,t)=>{let s=[...e.classList].filter((e=>Object.keys(this.#s).includes))[0];this.#e.querySelectorAll(`tbody tr:not(.child) td:nth-child(${t+1})`).forEach((t=>{s&&t.classList.add(s),s&&this.hiddenClasses().includes(s)?(e.classList.add("rpc-hidden"),t.classList.add("rpc-hidden")):(e.classList.remove("rpc-hidden"),t.classList.remove("rpc-hidden"))}))}))}#n(e,t,s){let l=e.querySelector("ul");for(let e of l.getElementsByTagName("li"))if(l.dataset.row==t&&e.dataset.column==s)return!0;return!1}#r(e){let t=document.createElement("li");return t.dataset.column=e.cellIndex,t.appendChild(Object.assign(document.createElement("span"),{classList:["rpc-child-title"],innerHTML:this.tableHeaders[e.cellIndex]})),t.appendChild(Object.assign(document.createElement("span"),{classList:["rpc-child-value"]})),e.children.length>0?t.children[1].append(...e.children):t.children[1].innerHTML=e.innerText,t}#i(e){clearTimeout(this.#l),this.#l=setTimeout(this.render.bind(this),this.#l)}} \ No newline at end of file diff --git a/src/rpc-table.css b/src/rpc-table.css new file mode 100644 index 0000000..c6b8c1b --- /dev/null +++ b/src/rpc-table.css @@ -0,0 +1,40 @@ +.rpc .rpc-hidden { + display: none; +} + +.rpc tbody > tr.has-child > .rpc-toggler::before { + content: ""; + margin-right: 0.5em; + display: inline-block; + box-sizing: border-box; + content: ""; + border-top: 5px solid transparent; + border-left: 10px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid transparent; + border-right: 0px solid transparent; +} + +.rpc tbody > tr.has-child.rpc-expanded > .rpc-toggler::before { + border-top: 10px solid rgba(0, 0, 0, 0.5); + border-left: 5px solid transparent; + border-bottom: 0px solid transparent; + border-right: 5px solid transparent; +} + +.rpc tbody > tr.child ul { + display: inline-block; + list-style-type: none; + margin: 0; + padding: 0; +} + +.rpc tbody > tr.child ul li { + border-bottom: 1px solid #efefef; + padding: 0.5em 0; +} + +.rpc tbody > tr.child span.rpc-child-title { + display: inline-block; + min-width: 75px; + font-weight: bold; +} \ No newline at end of file diff --git a/src/rpc-table.js b/src/rpc-table.js new file mode 100644 index 0000000..21cd161 --- /dev/null +++ b/src/rpc-table.js @@ -0,0 +1,258 @@ +class RpcTable { + /** @type {HTMLTableElement} */ + #table; + + /** @type {Array} */ + #children; + + /** @type {Object} */ + #breakpoints; + + /** + * Retrieves the headers of the table. + * @returns {Array} An array containing the innerText of each th element in the first row of the table. + */ + get tableHeaders() { + return [...this.#table.querySelectorAll("tr:nth-child(1) th")] + .map((node) => node.innerText); + } + + /** @type {number} */ + #resizeTimeout; + + /** + * @param {string} selector CSS selector + * @param {{breakpoints: Object, resizeTimeout: number}} options + */ + constructor(selector, options = {}) { + this.#table = document.querySelector(selector); + if (!this.#table) { + throw new Error("Invalid selector") + } + this.#table.classList.add("rpc"); + + this.#children = []; + this.#breakpoints = options.breakpoints || { + "collapse-xs": 576, + "collapse-sm": 768, + "collapse-md": 992, + "collapse-lg": 1200, + "collapse": Number.MAX_SAFE_INTEGER, + } + this.#resizeTimeout = options.resizeTimeout || 150; + + this.process(); + + this.#table.addEventListener("click", (event) => { + if (!event.target.classList.contains("rpc-toggler")) + return + + let tr = event.target.closest("tr"); + if (tr.classList.contains("rpc-expanded")) { + tr.nextSibling.remove() + } else { + tr.after(this.#children[tr.dataset.childIndex]) + } + + tr.classList.toggle("rpc-expanded") + }) + window.addEventListener("resize", this.#handleResize.bind(this)) + } + + /** + * Processes the table to create child rows. + * + * Creates child rows for each row in the table body that does not have the "child" class. + * Assigns a unique index to each child row. + * Renders the updated table. + */ + process() { + this.#children = []; + + this.#table.querySelectorAll("tbody tr:not(.child)").forEach((tr) => { + let child = Object.assign(document.createElement("tr"), { + classList: ["child"], + colspan: "100%", + innerHTML: `
        `, + }); + + + tr.querySelectorAll("td").forEach((td, i) => { + if (!td.classList.contains("rpc-hidden")) + return + child.querySelector("td > ul").appendChild(this.#createChildLi(td)) + }); + + tr.dataset.childIndex = this.#children.length; + tr.dataset.row = tr.rowIndex; + + this.#children.push(child); + }) + this.render(); + } + + /** + * Renders the table with updated child rows and responsive classes. + * + * Toggles the responsive class based on the visibility of child rows. + * Updates the child rows based on the visibility of cells in each row. + * Removes child rows that are no longer needed. + * Updates the toggler class for rows with child rows. + */ + render() { + this.#toggleResponsiveClass(); + + this.#table.querySelectorAll("tbody tr:not(.child)").forEach((tr) => { + let child = this.#children[tr.dataset.childIndex]; + /** @type {HTMLUListElement} */ + let childContainer = child.querySelector("tr > td > ul"); + + for (let td of tr.cells) { + if (td.classList.contains("rpc-hidden") + && !this.#inChild(child, tr.dataset.row, td.cellIndex) + ) { + // create child + let index = childContainer.children.length; + while (true) { + if (index == 0) { + childContainer.prepend(this.#createChildLi(td)); + break; + } + + index--; + if (td.cellIndex > childContainer.children[index].dataset.column) { + childContainer.children[index].after(this.#createChildLi(td)); + break; + } + } + continue; + } + + if (!td.classList.contains("rpc-hidden") + && this.#inChild(child, tr.dataset.row, td.cellIndex) + ) { + // remove child + for (let li of childContainer.getElementsByTagName("li")) { + if (li.dataset.column != td.cellIndex) { + continue; + } + + for (let row of this.#table.rows) { + if (row.dataset.row != tr.dataset.row) + continue; + + let span = li.querySelector(".rpc-child-value"); + if (span.children.length > 0) { + this.#table.rows[row.rowIndex].cells[parseInt(li.dataset.column)].append(...span.children); + } else { + this.#table.rows[row.rowIndex].cells[parseInt(li.dataset.column)].innerHTML = span.innerHTML; + } + li.remove(); + } + break; + } + continue; + } + } + + tr.querySelector('td.rpc-toggler')?.classList.remove("rpc-toggler"); + if (childContainer.children.length > 0) { + tr.classList.add("has-child") + tr.querySelector('td:not(.rpc-hidden)').classList.add("rpc-toggler"); + } else { + tr.classList.remove("has-child") + if (tr.classList.contains("rpc-expanded")) { + this.#table.rows[tr.rowIndex + 1].remove() + tr.classList.remove("rpc-expanded") + } + } + }); + } + + /** + * Retrieves the hidden classes based on the current window width and breakpoints. + * @returns {Array} An array of class names that are hidden based on the current window width. + */ + hiddenClasses() { + return Object.keys(this.#breakpoints) + .filter((k) => this.#breakpoints[k] >= window.innerWidth) + } + + /** + * Toggles the responsive class for table headers and cells based on the current window width and breakpoints. + */ + #toggleResponsiveClass() { + this.#table.querySelectorAll("thead > tr > th").forEach((th, i) => { + let responsive = [...th.classList].filter(c => Object.keys(this.#breakpoints).includes)[0] + + this.#table.querySelectorAll(`tbody tr:not(.child) td:nth-child(${i + 1})`).forEach((td) => { + if (responsive) + td.classList.add(responsive) + + if (responsive && this.hiddenClasses().includes(responsive)) { + th.classList.add("rpc-hidden") + td.classList.add("rpc-hidden") + } else { + th.classList.remove("rpc-hidden") + td.classList.remove("rpc-hidden") + } + }); + }); + } + + /** + * Checks if a cell is present in a child row. + * + * @param {HTMLElement} child - The child element. + * @param {number} row - The row value of the cell. + * @param {number} column - The column value of the cell. + * @returns {boolean} True if the cell is present in the child row, false otherwise. + */ + #inChild(child, row, column) { + let ul = child.querySelector("ul"); + for (let li of ul.getElementsByTagName("li")) { + if (ul.dataset.row == row && li.dataset.column == column) { + return true + } + } + return false + } + + /** + * Creates a child list item element for a given table cell. + * + * @param {HTMLTableCellElement} td - The table cell element. + * @returns {HTMLLIElement} The created list item element. + */ + #createChildLi(td) { + let li = document.createElement("li"); + li.dataset.column = td.cellIndex; + + li.appendChild(Object.assign(document.createElement("span"), { + classList: ["rpc-child-title"], + innerHTML: this.tableHeaders[td.cellIndex], + })); + + li.appendChild(Object.assign(document.createElement("span"), { + classList: ["rpc-child-value"], + })); + + if (td.children.length > 0) { + li.children[1].append(...td.children) + } else { + li.children[1].innerHTML = td.innerText + } + + return li + } + + /** + * Handle screen resize. + * + * @param {UIEvent} event + */ + #handleResize(event) { + clearTimeout(this.#resizeTimeout); + this.#resizeTimeout = setTimeout(this.render.bind(this), this.#resizeTimeout); + } +} \ No newline at end of file