summaryrefslogtreecommitdiffstats
path: root/grid/src
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2020-11-15 23:47:23 +0100
committerivarlovlie <git@ivarlovlie.no>2020-11-15 23:47:23 +0100
commita4bf3451bbfc6292f8d33d5241d693251d5a0d01 (patch)
tree1d136c532889acd3f9be3e419c5f35f5f7280491 /grid/src
parentbaa57ebf7f927d3d97eb4f538ed185d5d91fb731 (diff)
downloadweb-components-a4bf3451bbfc6292f8d33d5241d693251d5a0d01.tar.xz
web-components-a4bf3451bbfc6292f8d33d5241d693251d5a0d01.zip
add wip grid component
use cdn for bootstrap remove bloat from toaster
Diffstat (limited to 'grid/src')
-rw-r--r--grid/src/grid.ts244
1 files changed, 244 insertions, 0 deletions
diff --git a/grid/src/grid.ts b/grid/src/grid.ts
new file mode 100644
index 0000000..5fcf252
--- /dev/null
+++ b/grid/src/grid.ts
@@ -0,0 +1,244 @@
+export default class Grid {
+ private readonly canRender: boolean;
+ private readonly defaultOptions: GridConfiguration = {
+ id: "id",
+ debug: true,
+ columns: [],
+ data: [],
+ pageSize: 0,
+ };
+ private configuration: GridConfiguration;
+ private domElement: Element;
+ private currentPage: number = 0;
+
+ constructor(props: GridProps) {
+ this.setOptions(props);
+ this.validateOptions();
+ this.canRender = true;
+ }
+
+ private setOptions(props: GridProps = this.defaultOptions): void {
+ this.configuration = {} as GridConfiguration;
+ this.configuration.columns = [];
+ for (const column of props.columns) {
+ if (isGridColumn(column)) {
+ this.configuration.columns.push(column);
+ } else {
+ this.log("column is not of type GridColumn: " + JSON.stringify(column));
+ }
+ }
+ this.configuration.data = props.data ?? this.defaultOptions.data;
+ this.configuration.id = props.id ?? this.defaultOptions.id;
+ this.configuration.debug = props.debug ?? this.defaultOptions.debug;
+ this.configuration.pageSize = props.pageSize ?? this.defaultOptions.pageSize;
+ }
+
+ private validateOptions(): void {
+ if (this.configuration.data.length === undefined) {
+ throw new GridError("props.data.length is undefined");
+ }
+ if (this.configuration.columns.length === undefined || this.configuration.columns.length <= 0) {
+ throw new GridError("props.columns.length is undefined or <= 0");
+ }
+ }
+
+ private navigate(pageNumber) {
+ const maxPage = Math.ceil(this.configuration.data.length / this.configuration.pageSize - 1);
+ if (this.configuration.pageSize <= 0 || pageNumber < 0 || pageNumber === this.currentPage || pageNumber > maxPage) return;
+ this.log("Navigating to page: " + pageNumber);
+ const skipCount = this.configuration.pageSize * pageNumber;
+ const endIndex =
+ this.configuration.data.length < skipCount + this.configuration.pageSize
+ ? this.configuration.data.length
+ : skipCount + this.configuration.pageSize;
+ this.renderBody(this.configuration.data.slice(skipCount, endIndex));
+ this.currentPage = pageNumber;
+ }
+
+ private renderPaginator() {
+ if (this.configuration.pageSize === 0) return;
+ const nav = document.createElement("nav");
+ nav.className = "float-right user-select-none";
+ const ul = document.createElement("ul");
+ ul.className = "pagination";
+ const previousItem = document.createElement("li");
+ previousItem.className = "page-item";
+ previousItem.onclick = () => this.navigate(this.currentPage - 1);
+ const previousLink = document.createElement("span");
+ previousLink.style.cursor = "pointer";
+ previousLink.className = "page-link";
+ previousLink.innerText = "Forrige";
+ previousItem.appendChild(previousLink);
+ ul.appendChild(previousItem);
+
+ for (let i = 0; i < this.configuration.data.length / this.configuration.pageSize; i++) {
+ const item = document.createElement("li");
+ item.className = "page-item";
+ item.onclick = () => this.navigate(i);
+ const link = document.createElement("span");
+ link.className = "page-link";
+ link.style.cursor = "pointer";
+ link.innerText = (i + 1).toString();
+ item.appendChild(link);
+ ul.appendChild(item);
+ }
+
+ const nextItem = document.createElement("li");
+ nextItem.className = "page-item";
+ nextItem.onclick = () => this.navigate(this.currentPage + 1);
+ const nextLink = document.createElement("span");
+ nextLink.style.cursor = "pointer";
+ nextLink.className = "page-link";
+ nextLink.innerText = "Neste";
+ nextItem.appendChild(nextLink);
+
+ ul.appendChild(nextItem);
+ nav.appendChild(ul);
+ this.domElement.appendChild(nav);
+ }
+
+ private renderWrapper(): void {
+ const wrapper = document.createElement("table");
+ const thead = document.createElement("thead");
+ const tbody = document.createElement("tbody");
+ wrapper.className = "table";
+ wrapper.appendChild(thead);
+ wrapper.appendChild(tbody);
+ this.domElement.appendChild(wrapper);
+ }
+
+ private renderHead(): void {
+ const wrapper = this.domElement.querySelector("table thead");
+ wrapper.innerHTML = "";
+ const row = document.createElement("tr");
+ for (const col of this.configuration.columns) {
+ const th = document.createElement("th");
+ th.innerText = this.asString(col.columnName);
+ row.appendChild(th);
+ }
+ wrapper.appendChild(row);
+ }
+
+ private renderBody(data: Array<any> = null): void {
+ const wrapper = this.domElement.querySelector("table tbody");
+ wrapper.innerHTML = "";
+ let items = data ?? this.configuration.data;
+ if (this.configuration.pageSize !== 0) {
+ items = items.slice(0, this.configuration.pageSize);
+ }
+ for (const item of items) {
+ const row = document.createElement("tr");
+ row.dataset.id = item[this.configuration.id];
+ for (const val of this.configuration.columns) {
+ const cell = document.createElement("td");
+ if (typeof val.dataId === "string" && val.dataId.length > 0) {
+ val.cellValue = item[val.dataId];
+ }
+
+ if (typeof val.cellValue === "string" || typeof val.cellValue === "number") {
+ cell.innerText = val.cellValue;
+ } else if (val.cellValue instanceof Element) {
+ cell.appendChild(val.cellValue);
+ } else if (val.cellValue instanceof Function) {
+ const computed = val.cellValue(item);
+ if (computed instanceof Element) cell.appendChild(computed);
+ else if (typeof computed === "string" || typeof computed === "number") cell.innerText = computed as string;
+ }
+ row.appendChild(cell);
+ }
+ wrapper.appendChild(row);
+ }
+ }
+
+ private asString(val: string | Function, callbackData: any = undefined) {
+ if (val instanceof Function) {
+ return val(callbackData);
+ } else {
+ return val;
+ }
+ }
+
+ private static id(): string {
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
+ }
+
+ private log(message: any): void {
+ if (!this.configuration.debug) return;
+ console.log("Grid Debug: " + message);
+ }
+
+ public removeByID(id: string): void {
+ const itemIndex = this.configuration.data.findIndex((c) => c[this.configuration.id] === id);
+ if (itemIndex !== -1) {
+ delete this.configuration.data[itemIndex];
+ this.domElement.querySelector(`tr[data-id="${id}"]`).remove();
+ } else {
+ this.log("Grid does not contain id: " + id);
+ }
+ }
+
+ public refreshData() {
+ this.renderBody();
+ }
+
+ public render(el: Element): void {
+ if (this.canRender) {
+ this.log("Grid starting render", true);
+ this.domElement = el;
+ this.renderWrapper();
+ this.renderHead();
+ this.renderBody();
+ this.renderPaginator();
+ this.log("Grid was rendered", true);
+ } else {
+ throw new GridError("render is not allowed due to invalid props");
+ }
+ }
+}
+
+class GridError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "GridError";
+ }
+}
+
+function isArrayOfGridColumn(array: Array<GridColumn> | Array<string>): array is Array<GridColumn> {
+ try {
+ // @ts-ignore
+ return !(array.map((element) => "name" in element).indexOf(false) !== -1);
+ } catch (e) {
+ return false;
+ }
+}
+
+function isGridColumn(val: GridColumn | string): val is GridColumn {
+ try {
+ // @ts-ignore
+ return "columnName" in val && ("cellValue" in val || "dataId" in val);
+ } catch (e) {
+ return false;
+ }
+}
+
+interface GridProps {
+ id?: string;
+ debug?: boolean;
+ data: Array<any>;
+ columns: Array<GridColumn>;
+ pageSize?: number;
+}
+
+interface GridConfiguration {
+ id: string;
+ debug: boolean;
+ data: Array<any>;
+ columns: Array<GridColumn>;
+ pageSize: number;
+}
+
+interface GridColumn {
+ dataId?: string;
+ columnName: string | Function;
+ cellValue?: string | Function | Element;
+}