From 1fb71fe8994ec0443c7142538575ab9059d6aba3 Mon Sep 17 00:00:00 2001 From: bytedream <bytedream@protonmail.com> Date: Tue, 29 Aug 2023 00:43:15 +0200 Subject: [PATCH] make admin table resizable --- package-lock.json | 25 +++++++++++ package.json | 2 + src/lib/components/Input/Input.svelte | 17 ++++++-- src/lib/components/Input/Select.svelte | 7 ++- src/lib/components/utils.ts | 49 +++++++++++++++++++++ src/lib/permissions.ts | 4 +- src/routes/+layout.svelte | 8 ++-- src/routes/admin/+layout.svelte | 4 +- src/routes/admin/admin/+page.svelte | 60 +++++++++++++++++--------- src/routes/admin/admin/+server.ts | 2 + svelte.config.js | 4 +- 11 files changed, 148 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8f8388..0d594cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,13 @@ "postcss": "^8.4.27", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", + "sass": "^1.66.1", "svelte": "^4.0.5", "svelte-check": "^3.4.3", "svelte-heros-v2": "^0.9.3", "svelte-local-storage-store": "^0.6.0", "svelte-multicssclass": "^2.1.1", + "svelte-preprocess": "^5.0.4", "tailwindcss": "^3.3.3", "tslib": "^2.4.1", "typescript": "^5.0.0", @@ -2854,6 +2856,12 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4391,6 +4399,23 @@ "rimraf": "bin.js" } }, + "node_modules/sass": { + "version": "1.66.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", diff --git a/package.json b/package.json index e795aad..0bb4457 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "postcss": "^8.4.27", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", + "sass": "^1.66.1", "svelte": "^4.0.5", "svelte-check": "^3.4.3", "svelte-heros-v2": "^0.9.3", "svelte-local-storage-store": "^0.6.0", "svelte-multicssclass": "^2.1.1", + "svelte-preprocess": "^5.0.4", "tailwindcss": "^3.3.3", "tslib": "^2.4.1", "typescript": "^5.0.0", diff --git a/src/lib/components/Input/Input.svelte b/src/lib/components/Input/Input.svelte index 099c5f9..c810459 100644 --- a/src/lib/components/Input/Input.svelte +++ b/src/lib/components/Input/Input.svelte @@ -28,7 +28,11 @@ <div class={type === 'submit' && disabled ? 'cursor-not-allowed' : ''}> {#if type === 'submit'} <input - class={`btn btn-${size}`} + class="btn" + class:btn-xs={size === 'xs'} + class:btn-sm={size === 'sm'} + class:btn-md={size === 'md'} + class:btn-lg={size === 'lg'} {id} type="submit" {disabled} @@ -49,9 +53,16 @@ {/if} <div class="relative flex items-center" class:sm:max-w-[16rem]={type !== 'checkbox'}> <input - class={type === 'checkbox' ? `checkbox-${size}` : `input-${size}`} class:checkbox={type === 'checkbox'} - class:input,input-bordered,w-[100%]={type !== 'checkbox'} + class:checkbox-xs={type === 'checkbox' && size === 'xs'} + class:checkbox-sm={type === 'checkbox' && size === 'sm'} + class:checkbox-md={type === 'checkbox' && size === 'md'} + class:checkbox-lg={type === 'checkbox' && size === 'lg'} + class:input,input-bordered,w-full={type !== 'checkbox'} + class:input-xs={type !== 'checkbox' && size === 'xs'} + class:input-sm={type !== 'checkbox' && size === 'sm'} + class:input-md={type !== 'checkbox' && size === 'md'} + class:input-lg={type !== 'checkbox' && size === 'lg'} class:pr-11={initialType === 'password'} {id} {name} diff --git a/src/lib/components/Input/Select.svelte b/src/lib/components/Input/Select.svelte index 294dd7d..5cfff11 100644 --- a/src/lib/components/Input/Select.svelte +++ b/src/lib/components/Input/Select.svelte @@ -6,6 +6,7 @@ export let notice: string | null = null; export let required = false; export let disabled = false; + export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md'; </script> <div> @@ -20,7 +21,11 @@ </label> {/if} <select - class="input input-bordered w-[100%] sm:max-w-[16rem]" + class="select select-bordered w-[100%] sm:max-w-[16rem]" + class:select-xs={size === 'xs'} + class:select-sm={size === 'sm'} + class:select-md={size === 'md'} + class:select-lg={size === 'lg'} {id} {name} {required} diff --git a/src/lib/components/utils.ts b/src/lib/components/utils.ts index 9d1296e..9bb5bc0 100644 --- a/src/lib/components/utils.ts +++ b/src/lib/components/utils.ts @@ -3,3 +3,52 @@ export async function buttonTriggeredRequest<T>(e: MouseEvent, promise: Promise< await promise; (e.target as HTMLButtonElement).disabled = false; } + +export function resizeTableColumn(event: MouseEvent, dragOffset: number) { + const element = event.target as HTMLTableCellElement; + const rect = element.getBoundingClientRect(); + + const posX = event.clientX - rect.left; + const offset = rect.width - event.clientX; + if (posX <= dragOffset || posX >= rect.width - dragOffset) { + // do not resize if resize request is on the table left or right + if ( + (posX <= dragOffset && !element.previousElementSibling) || + (posX >= rect.width - dragOffset && !element.nextElementSibling) + ) { + return; + } + + const table = element.parentElement!.parentElement!.parentElement as HTMLTableElement; + let resizeRow: HTMLTableRowElement; + if (table.tBodies[0].rows[0].hidden) { + resizeRow = table.rows[0]; + } else { + resizeRow = table.tBodies[0].insertRow(0); + resizeRow.hidden = true; + for (let i = 0; i < table.rows[0].cells.length; i++) { + resizeRow.insertCell(); + } + + // insert an additional to keep the zebra in place pattern which might be applied + const zebraGhostRow = table.tBodies[0].insertRow(0); + zebraGhostRow.hidden = true; + } + + const resizeElement = + resizeRow.cells[element.cellIndex - ((posX <= dragOffset) as unknown as number)]; + + // eslint-disable-next-line svelte/no-inner-declarations,no-inner-declarations + function resize(e: MouseEvent) { + document.body.style.cursor = 'col-resize'; + resizeElement.style.width = `${offset + e.clientX}px`; + } + + document.addEventListener('mousemove', resize); + + document.addEventListener('mouseup', () => { + document.removeEventListener('mousemove', resize); + document.body.style.cursor = 'initial'; + }); + } +} diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts index 4cb2866..6e96bb8 100644 --- a/src/lib/permissions.ts +++ b/src/lib/permissions.ts @@ -6,9 +6,11 @@ export class Permissions { readonly value: number; - constructor(value: number | number[]) { + constructor(value: number | number[] | null) { if (typeof value == 'number') { this.value = value; + } else if (value == null) { + this.value = 0; } else { let finalValue = 0; for (const v of Object.values(value)) { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e310ac0..ce5957b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -92,9 +92,8 @@ </main> <nav> <div - class={onAdminPage - ? 'fixed bottom-4 right-4 group/menu-bar flex flex-col-reverse justify-center items-center' - : 'fixed bottom-4 right-4 group/menu-bar flex flex-col-reverse justify-center items-center sm:left-4 sm:right-[initial]'} + class="fixed bottom-4 right-4 sm:left-4 sm:right-[initial] group/menu-bar flex flex-col-reverse justify-center items-center" + class:hidden={onAdminPage} bind:this={nav} > <button @@ -145,8 +144,7 @@ <ul class="flex flex-col bg-base-200 rounded"> {#each navPaths as navPath, i} <li - class="flex justify-center tooltip tooltip-left" - class:sm:tooltip-right={!onAdminPage} + class="flex justify-center tooltip tooltip-left sm:tooltip-right" data-tip={navPath.name} > <a diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index 363e5b6..982f902 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -19,7 +19,7 @@ {#if $page.url.pathname !== `${env.PUBLIC_BASE_PATH}/admin/login`} <div class="flex h-screen"> - <div class="h-full"> + <div class="h-full w-max"> <ul class="menu p-4 w-fit h-full bg-base-200 text-base-content"> <li> <a href="{env.PUBLIC_BASE_PATH}/admin/admin"> @@ -35,7 +35,7 @@ </li> </ul> </div> - <div class="h-full w-full"> + <div class="h-full w-full overflow-scroll"> <slot /> </div> </div> diff --git a/src/routes/admin/admin/+page.svelte b/src/routes/admin/admin/+page.svelte index a159a4e..7fcd397 100644 --- a/src/routes/admin/admin/+page.svelte +++ b/src/routes/admin/admin/+page.svelte @@ -6,7 +6,7 @@ import { Permissions } from '$lib/permissions'; import { env } from '$env/dynamic/public'; import ErrorToast from '$lib/components/Toast/ErrorToast.svelte'; - import { buttonTriggeredRequest } from '$lib/components/utils'; + import { buttonTriggeredRequest, resizeTableColumn } from '$lib/components/utils'; import { goto } from '$app/navigation'; let allPermissionBadges = { @@ -30,7 +30,9 @@ }) }); if (response.ok) { - data.admins.push(await response.json()); + let res = await response.json(); + res.permissions = new Permissions(res.permissions).asArray(); + data.admins.push(res); data.admins = data.admins; } else { throw new Error(); @@ -89,28 +91,21 @@ let permissions = new Permissions(data.permissions); </script> -<table class="table table-zebra"> - <colgroup> - <col span="1" style="width: 5%" /> - <col span="1" style="width: 25%" /> - <col span="1" style="width: 25%" /> - <col span="1" style="width: 30%" /> - <col span="1" style="width: 15%" /> - </colgroup> +<table class="table table-zebra w-full"> <thead> <tr> - <th /> - <th>Benutzername</th> - <th>Passwort</th> - <th>Berechtigungen</th> - <th /> + <th on:mousedown={(e) => resizeTableColumn(e, 5)} /> + <th on:mousedown={(e) => resizeTableColumn(e, 5)}>Benutzername</th> + <th on:mousedown={(e) => resizeTableColumn(e, 5)}>Passwort</th> + <th on:mousedown={(e) => resizeTableColumn(e, 5)}>Berechtigungen</th> + <th on:mousedown={(e) => resizeTableColumn(e, 5)} /> </tr> </thead> <tbody> {#each data.admins as admin, i} <tr> - <td>{i}</td> - <td + <td on:mousedown={(e) => resizeTableColumn(e, 5)}>{i}</td> + <td on:mousedown={(e) => resizeTableColumn(e, 5)} ><Input type="text" bind:value={admin.username} @@ -118,7 +113,7 @@ size="sm" /></td > - <td + <td on:mousedown={(e) => resizeTableColumn(e, 5)} ><Input type="password" bind:value={admin.password} @@ -127,14 +122,14 @@ size="sm" /></td > - <td + <td on:mousedown={(e) => resizeTableColumn(e, 5)} ><Badges bind:value={admin.permissions} available={allPermissionBadges} disabled={!permissions.adminWrite() || !admin.edit} /></td > - <td> + <td on:mousedown={(e) => resizeTableColumn(e, 5)}> <div> {#if admin.edit} <span class="w-min" class:cursor-not-allowed={!permissions.adminWrite()}> @@ -245,3 +240,28 @@ <ErrorToast show={errorMessage !== ''}> <span /> </ErrorToast> + +<style lang="scss"> + thead tr th, + tbody tr td { + @apply relative; + + &:not(:first-child) { + @apply border-l-[1px] border-dashed; + + &::before { + @apply absolute left-0 bottom-0 h-full w-[5px] cursor-col-resize; + content: ''; + } + } + + &:not(:last-child) { + @apply border-r-[1px] border-dashed border-base-300; + + &::after { + @apply absolute right-0 bottom-0 h-full w-[5px] cursor-col-resize; + content: ''; + } + } + } +</style> diff --git a/src/routes/admin/admin/+server.ts b/src/routes/admin/admin/+server.ts index 7f0cb19..732c748 100644 --- a/src/routes/admin/admin/+server.ts +++ b/src/routes/admin/admin/+server.ts @@ -34,6 +34,8 @@ export const POST = (async ({ request, cookies }) => { permissions: new Permissions(permissions) }); + delete admin.dataValues.password; + return new Response(JSON.stringify(admin), { status: 201 }); diff --git a/svelte.config.js b/svelte.config.js index 2214c60..940bed5 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,11 +1,11 @@ import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/kit/vite'; +import preprocess from 'svelte-preprocess'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors // for more information about preprocessors - preprocess: vitePreprocess(), + preprocess: preprocess(), kit: { // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.