f047_Edit-der-Namen #15
123
src/lib/components/EditableItem.svelte
Normal file
123
src/lib/components/EditableItem.svelte
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Edit from '$lib/icons/Edit.svelte';
|
||||||
|
import Trash from '$lib/icons/Trash.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { validateInput } from '$lib/helper/error-utils';
|
||||||
|
|
||||||
|
interface ListItem {
|
||||||
|
name: string;
|
||||||
|
token?: string;
|
||||||
|
// add other properties as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export let value: string = '';
|
||||||
|
export let variant: '' | 'casename' | 'crimename' = ''; // casename | crimename
|
||||||
|
export let existings: ListItem[];
|
||||||
|
export let id: number;
|
||||||
|
export let editable: boolean = true;
|
||||||
|
export let editing: boolean;
|
||||||
|
|
||||||
|
console.log('Debug editing', editing);
|
||||||
|
|
||||||
|
const existingNames = existings.map((item) => item.name);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
editSart: {};
|
||||||
|
save: {};
|
||||||
|
delete: {};
|
||||||
|
cancel: void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let internalValue = value;
|
||||||
|
let oldValue = value;
|
||||||
|
let showWarning = false;
|
||||||
|
let duplicate = false;
|
||||||
|
|
||||||
|
let errors: string[] = [];
|
||||||
|
let errorText = '';
|
||||||
|
|
||||||
|
$: errors = validateInput(oldValue, internalValue, { minLength: 3, existingNames });
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
oldValue = value;
|
||||||
|
internalValue = value;
|
||||||
|
editing = true;
|
||||||
|
dispatch('editSart', { id, value, variant, editing });
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
internalValue = oldValue;
|
||||||
|
editing = false;
|
||||||
|
showWarning = false;
|
||||||
|
|
||||||
|
console.log('Abgebrochen');
|
||||||
|
|
||||||
|
dispatch('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEdit(ev?: MouseEvent | KeyboardEvent) {
|
||||||
|
ev?.preventDefault();
|
||||||
|
ev?.stopPropagation();
|
||||||
|
|
||||||
|
const name = internalValue.trim();
|
||||||
|
|
||||||
|
if (errors.length !== 0) {
|
||||||
|
showWarning = true;
|
||||||
|
console.log('Abgebrochen', errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
showWarning = true;
|
||||||
|
console.log('Abgebrochen', showWarning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingNames.includes(name) && name !== oldValue.trim()) {
|
||||||
|
showWarning = true;
|
||||||
|
console.log('Abgebrochen', showWarning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editing = false;
|
||||||
|
showWarning = false;
|
||||||
|
duplicate = false;
|
||||||
|
|
||||||
|
dispatch('save', { newValue: internalValue, oldValue, variant });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteItem() {
|
||||||
|
dispatch('delete', { value, variant });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') saveEdit(e);
|
||||||
|
if (e.key === 'Escape') cancelEdit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="" {...$$restProps}>
|
||||||
|
<div class="flex gap-x-2">
|
||||||
|
{#if editing}
|
||||||
|
<input
|
||||||
|
bind:value={internalValue}
|
||||||
|
on:keydown={handleKey}
|
||||||
|
on:blur|preventDefault|stopPropagation={cancelEdit}
|
||||||
|
class=""
|
||||||
|
class:bg-red-200={(showWarning && editing) || errors.length !== 0}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span>{value}</span>
|
||||||
|
{#if !editing && editable}
|
||||||
|
<button on:click|preventDefault|stopPropagation={startEdit}>
|
||||||
|
<Edit />
|
||||||
|
</button>
|
||||||
|
<button on:click|preventDefault={deleteItem}>
|
||||||
|
<Trash />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if editing && errors}
|
||||||
|
<p class="text-red-600 text-sm mt-1 font-medium">{errors[0]}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Cube from '$lib/icons/Cube.svelte';
|
|
||||||
import Edit from '$lib/icons/Edit.svelte';
|
|
||||||
import Trash from '$lib/icons/Trash.svelte';
|
|
||||||
import shortenFileSize from '$lib/helper/shortenFileSize';
|
|
||||||
import timeElapsed from '$lib/helper/timeElapsed';
|
|
||||||
|
|
||||||
export let data;
|
|
||||||
export let i;
|
|
||||||
|
|
||||||
let crimesList = data.crimesList;
|
|
||||||
let item = crimesList[i];
|
|
||||||
|
|
||||||
let admin = data.user?.admin;
|
|
||||||
let link = `/view/${item.prefix}/${item.name}?token=${data.caseToken}`;
|
|
||||||
|
|
||||||
console.log('Debug Mina', crimesList, data);
|
|
||||||
|
|
||||||
function editName() {
|
|
||||||
console.log('Edit Name');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCase() {
|
|
||||||
console.log('Delete Case');
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkIsEmpty() {
|
|
||||||
if (!item.name) console.log('Das Feld ist leer');
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelEdit() {
|
|
||||||
console.log('Bearbeitung abgebrochen');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a href={link} class="flex justify-between items-center gap-x-6 py-5">
|
|
||||||
<div class=" flex gap-x-4">
|
|
||||||
<Cube class="h-auto" />
|
|
||||||
<div class="min-w-0 flex-auto">
|
|
||||||
{#if admin}
|
|
||||||
<input
|
|
||||||
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
|
|
||||||
type="text"
|
|
||||||
value={item.name}
|
|
||||||
on:click|preventDefault={(ev) => {
|
|
||||||
editName();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-2"
|
|
||||||
on:click|preventDefault={(ev) => {
|
|
||||||
editName();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="p-2"
|
|
||||||
on:click|preventDefault={async (ev) => {
|
|
||||||
deleteCase();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1">
|
|
||||||
{item.name}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<p class="mt-1 truncate text-xs leading-5 text-gray-500">{shortenFileSize(item.size)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hidden sm:flex sm:flex-col sm:items-end">
|
|
||||||
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
|
|
||||||
|
|
||||||
<p class="mt-1 text-xs leading-5 text-gray-500">
|
|
||||||
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
|
|
||||||
>{timeElapsed(new Date(item.lastModified))}</time
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
9
src/lib/helper/error-utils.ts
Normal file
9
src/lib/helper/error-utils.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function validateInput(oldValue:string, value: string, options: { minLength?: number; existingNames?: string[] }) {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (!value.trim()) errors.push('Feld darf nicht leer sein');
|
||||||
|
if (options.existingNames?.includes(value) && oldValue !== value)
|
||||||
|
errors.push('Name existiert bereits');
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Trash from '$lib/icons/Trash.svelte';
|
|
||||||
import Folder from '$lib/icons/Folder.svelte';
|
import Folder from '$lib/icons/Folder.svelte';
|
||||||
import type { PageData } from '../$types';
|
import EditableItem from '$lib/components/EditableItem.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data;
|
||||||
|
export let editingId: number;
|
||||||
|
|
||||||
const caseList = data.caseList;
|
interface ListItem {
|
||||||
|
jared
commented
Man könnte einen Ordner für type anlegen src/lib/types/... Man könnte einen Ordner für type anlegen src/lib/types/...
mina
commented
s.u. s.u.
|
|||||||
|
name: string;
|
||||||
|
token?: string;
|
||||||
|
// add other properties as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseList: ListItem[] = data.caseList;
|
||||||
|
|
||||||
async function delete_item(ev: Event) {
|
async function delete_item(ev: Event) {
|
||||||
let delete_item = window.confirm('Bist du sicher?');
|
let delete_item = window.confirm('Bist du sicher?');
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
const target = ev.currentTarget as HTMLElement | null;
|
const target = ev.currentTarget as HTMLElement | null;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
let filename = target.id.split('del__')[1];
|
let filename = target.id.split('del__')[1];
|
||||||
|
|
||||||
// delete request
|
// delete request
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
@@ -44,27 +50,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mx-auto flex justify-center max-w-7xl h-full">
|
<div class="mx-auto flex justify-center max-w-7xl h-full">
|
||||||
<ul role="list" class="divide-y divide-gray-100">
|
<ul role="list" class="divide-y divide-gray-100">
|
||||||
{#each caseList as item}
|
{#each caseList as item, i}
|
||||||
<li>
|
<li>
|
||||||
<a href="/list/{item.name}?token={item.token}" class="flex justify-between gap-x-6 py-5">
|
<a href="/list/{item.name}?token={item.token}" class="flex justify-between gap-x-6 py-5">
|
||||||
<div class="flex gap-x-4">
|
<div class="flex gap-x-4">
|
||||||
<!-- Ordner -->
|
<!-- Ordner -->
|
||||||
<Folder />
|
<Folder />
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
<span class="text-sm font-semibold leading-6 text-gray-900">{item.name}</span>
|
<EditableItem
|
||||||
<!-- Delete button -->
|
class=""
|
||||||
<button
|
id={i}
|
||||||
style="padding: 2px"
|
value={item.name}
|
||||||
id="del__{item.name}"
|
editing={editingId === i}
|
||||||
on:click|preventDefault={delete_item}
|
on:editStart={() => (editingId = i)}
|
||||||
aria-label="Vorgang {item.name} löschen"
|
variant="casename"
|
||||||
>
|
existings={caseList}
|
||||||
<Trash />
|
on:save={(e) => console.log('Gespeichert:', e.detail)}
|
||||||
</button>
|
on:delete={(e) => {
|
||||||
|
console.log('Gelöscht:', e.detail);
|
||||||
|
delete_item(e);
|
||||||
|
}}
|
||||||
|
></EditableItem>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:flex sm:flex-col sm:items-end">
|
||||||
|
<p class="text-sm leading-6 text-gray-900">Vorgang</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="hidden sm:flex sm:flex-col sm:items-end">
|
|
||||||
<p class="text-sm leading-6 text-gray-900">Vorgang</p>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -6,14 +6,17 @@
|
|||||||
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
|
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
|
||||||
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
|
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
|
||||||
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
|
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
|
||||||
import ListItem from '$lib/components/ListItem.svelte';
|
import EditableItem from '$lib/components/EditableItem.svelte';
|
||||||
|
import Cube from '$lib/icons/Cube.svelte';
|
||||||
|
import shortenFileSize from '$lib/helper/shortenFileSize.js';
|
||||||
|
import timeElapsed from '$lib/helper/timeElapsed.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
interface ListItem {
|
interface ListItem {
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size?: number;
|
||||||
lastModified: string | number | Date;
|
lastModified?: string | number | Date | undefined;
|
||||||
show_button?: boolean;
|
show_button?: boolean;
|
||||||
// add other properties as needed
|
// add other properties as needed
|
||||||
}
|
}
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
$: inProgress;
|
$: inProgress;
|
||||||
let err = false;
|
let err = false;
|
||||||
$: err;
|
$: err;
|
||||||
|
let editingId: number;
|
||||||
|
// let admin = data?.user?.admin;
|
||||||
|
let admin = true;
|
||||||
|
|
||||||
let rename_input;
|
let rename_input;
|
||||||
$: rename_input;
|
$: rename_input;
|
||||||
@@ -36,21 +42,6 @@
|
|||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// function defocus_element(i: number) {
|
|
||||||
// let item = crimesList[i];
|
|
||||||
// let text_field_id = `label__${item.name}`;
|
|
||||||
|
|
||||||
// let text_field = document.getElementById(text_field_id);
|
|
||||||
// if (text_field) {
|
|
||||||
// text_field.setAttribute('contenteditable', 'false');
|
|
||||||
// text_field.textContent = item.name;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // reshow button
|
|
||||||
// crimesList[i].show_button = true;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function handle_input(ev: KeyboardEvent, i: number) {
|
async function handle_input(ev: KeyboardEvent, i: number) {
|
||||||
let item = crimesList[i];
|
let item = crimesList[i];
|
||||||
if (ev.key == 'Escape') {
|
if (ev.key == 'Escape') {
|
||||||
@@ -131,7 +122,7 @@
|
|||||||
<ul class="divide-y divide-gray-100">
|
<ul class="divide-y divide-gray-100">
|
||||||
{#each crimesList as item, i}
|
{#each crimesList as item, i}
|
||||||
<li>
|
<li>
|
||||||
<!-- <a
|
<a
|
||||||
href="/view/{$page.params.vorgang}/{item.name}?token={token}"
|
href="/view/{$page.params.vorgang}/{item.name}?token={token}"
|
||||||
class=" flex justify-between gap-x-6 py-5"
|
class=" flex justify-between gap-x-6 py-5"
|
||||||
aria-label="zum 3D-modell"
|
aria-label="zum 3D-modell"
|
||||||
@@ -139,106 +130,41 @@
|
|||||||
<div class=" flex gap-x-4">
|
<div class=" flex gap-x-4">
|
||||||
<Cube />
|
<Cube />
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
{#if data?.user?.admin}
|
{#if admin}
|
||||||
<span
|
<EditableItem
|
||||||
id="label__{item.name}"
|
class="bg-lred-500"
|
||||||
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
|
id={i}
|
||||||
contenteditable={!item.show_button}
|
value={item.name}
|
||||||
role="textbox"
|
editing={editingId === i}
|
||||||
tabindex="0"
|
on:editStart={() => (editingId = i)}
|
||||||
aria-label="Dateiname bearbeiten"
|
variant="crimename"
|
||||||
on:focusout={() => {
|
existings={crimesList}
|
||||||
defocus_element(i);
|
on:save={(e) => console.log('Gespeichert:', e.detail)}
|
||||||
}}
|
on:delete={(e) => console.log('Gelöscht:', e.detail)}
|
||||||
on:keydown|stopPropagation={// event needed to identify ID
|
></EditableItem>
|
||||||
// TO-DO: check if event is needed or if index is sufficient
|
|
||||||
async (ev) => {
|
|
||||||
handle_input(ev, i);
|
|
||||||
}}>{item.name}</span
|
|
||||||
>
|
|
||||||
|
|
||||||
{#if item.show_button}
|
|
||||||
<button
|
|
||||||
style="padding: 2px"
|
|
||||||
id="edit__{item.name}"
|
|
||||||
aria-label="Datei umbenennen"
|
|
||||||
on:click|preventDefault={(ev) => {
|
|
||||||
let text_field_id = `label__${item.name}`;
|
|
||||||
|
|
||||||
let text_field = document.getElementById(text_field_id);
|
|
||||||
if (text_field) {
|
|
||||||
text_field.setAttribute('contenteditable', 'true');
|
|
||||||
text_field.focus();
|
|
||||||
text_field.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide button
|
|
||||||
item.show_button = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
style="padding: 2px"
|
|
||||||
id="del__{item.name}"
|
|
||||||
on:click|preventDefault={async (ev) => {
|
|
||||||
let delete_item = window.confirm('Bist du sicher?');
|
|
||||||
|
|
||||||
if (delete_item) {
|
|
||||||
// bucket: tatort, name: <vorgang>/item-name
|
|
||||||
let vorgang = $page.params.vorgang;
|
|
||||||
let filename = '';
|
|
||||||
if (ev && ev.currentTarget && (ev.currentTarget as HTMLElement).id) {
|
|
||||||
filename = (ev.currentTarget as HTMLElement).id.split('del__')[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete request
|
|
||||||
// --------------
|
|
||||||
|
|
||||||
let url = new URL($page.url);
|
|
||||||
url.pathname += `/${filename}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`api/${url}`, { method: 'DELETE' });
|
|
||||||
if (response.status == 204) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.log(error.message);
|
|
||||||
} else {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label="Datei löschen"
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
|
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
|
||||||
>{item.name}</span
|
>{item.name}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
|
{#if item.size}
|
||||||
{shortenFileSize(item.size)}
|
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
|
||||||
</p>
|
{shortenFileSize(item.size)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden sm:flex sm:flex-col sm:items-end">
|
<div class="hidden sm:flex sm:flex-col sm:items-end">
|
||||||
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
|
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
|
||||||
<p class="mt-1 text-xs leading-5 text-gray-500">
|
{#if item.lastModified}
|
||||||
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
|
<p class="mt-1 text-xs leading-5 text-gray-500">
|
||||||
>{timeElapsed(new Date(item.lastModified))}</time
|
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
|
||||||
>
|
>{timeElapsed(new Date(item.lastModified))}</time
|
||||||
</p>
|
>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a> -->
|
</a>
|
||||||
<ListItem {data} {i}></ListItem>
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user
Grundsätzlich wird hier der Value in diesem Fall ein Name validiert. Es ist keine generische validateInput Funktion. Daher sollte sie entsprechend benannt werden. Darüber hinaus wird sie nur in EditableItem verwendet. Daher ist ggf. ein Auslagern nicht sinnvoll, oder soll sie noch wo anders verwendet werden?
Werde ich nochmal überarbeiten