244 lines
8.5 KiB
Svelte
244 lines
8.5 KiB
Svelte
<script>
|
|
import { SchemaField } from "pocketbase";
|
|
import ApiClient from "@/utils/ApiClient";
|
|
import CommonHelper from "@/utils/CommonHelper";
|
|
import tooltip from "@/actions/tooltip";
|
|
import Field from "@/components/base/Field.svelte";
|
|
import Draggable from "@/components/base/Draggable.svelte";
|
|
import UploadedFilePreview from "@/components/base/UploadedFilePreview.svelte";
|
|
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
|
import { onMount } from "svelte";
|
|
|
|
export let record;
|
|
export let value = "";
|
|
export let uploadedFiles = []; // Array<File> array
|
|
export let deletedFileIndexes = []; // Array<int> array
|
|
export let field = new SchemaField();
|
|
|
|
let fileInput;
|
|
let filesListElem;
|
|
let isDragOver = false;
|
|
let fileToken = "";
|
|
|
|
// normalize uploadedFiles type
|
|
$: if (!Array.isArray(uploadedFiles)) {
|
|
uploadedFiles = CommonHelper.toArray(uploadedFiles);
|
|
}
|
|
|
|
// normalize delited file indexes
|
|
$: if (!Array.isArray(deletedFileIndexes)) {
|
|
deletedFileIndexes = CommonHelper.toArray(deletedFileIndexes);
|
|
}
|
|
|
|
$: isMultiple = field.options?.maxSelect > 1;
|
|
|
|
$: if (CommonHelper.isEmpty(value)) {
|
|
value = isMultiple ? [] : "";
|
|
}
|
|
|
|
$: valueAsArray = CommonHelper.toArray(value);
|
|
|
|
$: maxReached =
|
|
(valueAsArray.length || uploadedFiles.length) &&
|
|
field.options?.maxSelect <= valueAsArray.length + uploadedFiles.length - deletedFileIndexes.length;
|
|
|
|
$: if (uploadedFiles !== -1 || deletedFileIndexes !== -1) {
|
|
triggerListChange();
|
|
}
|
|
|
|
function restoreExistingFile(valueIndex) {
|
|
CommonHelper.removeByValue(deletedFileIndexes, valueIndex);
|
|
deletedFileIndexes = deletedFileIndexes;
|
|
}
|
|
|
|
function removeExistingFile(valueIndex) {
|
|
CommonHelper.pushUnique(deletedFileIndexes, valueIndex);
|
|
deletedFileIndexes = deletedFileIndexes;
|
|
}
|
|
|
|
function removeNewFile(index) {
|
|
if (!CommonHelper.isEmpty(uploadedFiles[index])) {
|
|
uploadedFiles.splice(index, 1);
|
|
}
|
|
uploadedFiles = uploadedFiles;
|
|
}
|
|
|
|
// emulate native change event
|
|
function triggerListChange() {
|
|
filesListElem?.dispatchEvent(
|
|
new CustomEvent("change", {
|
|
detail: { value, uploadedFiles, deletedFileIndexes },
|
|
bubbles: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
function dropHandler(e) {
|
|
e.preventDefault();
|
|
|
|
isDragOver = false;
|
|
|
|
const files = e.dataTransfer?.files || [];
|
|
|
|
if (maxReached || !files.length || e.dataTransfer?.effectAllowed != "copy") {
|
|
return;
|
|
}
|
|
|
|
for (const file of files) {
|
|
const currentTotal = valueAsArray.length + uploadedFiles.length - deletedFileIndexes.length;
|
|
|
|
if (field.options?.maxSelect <= currentTotal) {
|
|
break;
|
|
}
|
|
|
|
uploadedFiles.push(file);
|
|
}
|
|
|
|
uploadedFiles = uploadedFiles;
|
|
}
|
|
|
|
onMount(async () => {
|
|
fileToken = await ApiClient.getAdminFileToken(record.collectionId);
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="block"
|
|
on:dragover|preventDefault={(e) => {
|
|
if (e.dataTransfer.effectAllowed != "copy") {
|
|
return; // not a file drag
|
|
}
|
|
isDragOver = true;
|
|
}}
|
|
on:dragleave={() => {
|
|
isDragOver = false;
|
|
}}
|
|
on:drop={dropHandler}
|
|
>
|
|
<Field
|
|
class="
|
|
form-field form-field-list form-field-file
|
|
{field.required ? 'required' : ''}
|
|
{isDragOver ? 'dragover' : ''}
|
|
"
|
|
name={field.name}
|
|
let:uniqueId
|
|
>
|
|
<label for={uniqueId}>
|
|
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
|
<span class="txt">{field.name}</span>
|
|
</label>
|
|
|
|
<div bind:this={filesListElem} class="list">
|
|
{#each valueAsArray as filename, i (filename + record.id)}
|
|
{@const isDeleted = deletedFileIndexes.includes(i)}
|
|
<Draggable
|
|
bind:list={value}
|
|
group={field.name + "_uploaded"}
|
|
index={i}
|
|
disabled={!isMultiple}
|
|
let:dragging
|
|
let:dragover
|
|
>
|
|
<div class="list-item" class:dragging class:dragover>
|
|
<div class:fade={deletedFileIndexes.includes(i)}>
|
|
<RecordFileThumb {record} {filename} />
|
|
</div>
|
|
|
|
<div class="content">
|
|
<a
|
|
draggable={false}
|
|
href={ApiClient.files.getUrl(record, filename, { token: fileToken })}
|
|
class="txt-ellipsis {isDeleted
|
|
? 'txt-strikethrough txt-hint'
|
|
: 'link-primary'}"
|
|
title="Download"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{filename}
|
|
</a>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
{#if deletedFileIndexes.includes(i)}
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-danger btn-transparent"
|
|
on:click={() => restoreExistingFile(i)}
|
|
>
|
|
<span class="txt">Restore</span>
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn btn-transparent btn-hint btn-sm btn-circle btn-remove"
|
|
use:tooltip={"Remove file"}
|
|
on:click={() => removeExistingFile(i)}
|
|
>
|
|
<i class="ri-close-line" />
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Draggable>
|
|
{/each}
|
|
|
|
{#each uploadedFiles as file, i}
|
|
<Draggable
|
|
bind:list={uploadedFiles}
|
|
group={field.name + "_new"}
|
|
index={i}
|
|
disabled={!isMultiple}
|
|
let:dragging
|
|
let:dragover
|
|
>
|
|
<div class="list-item" class:dragging class:dragover>
|
|
<figure class="thumb">
|
|
<UploadedFilePreview {file} />
|
|
</figure>
|
|
<div class="filename m-r-auto" title={file.name}>
|
|
<small class="label label-success m-r-5">New</small>
|
|
<span class="txt">{file.name}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-transparent btn-hint btn-sm btn-circle btn-remove"
|
|
use:tooltip={"Remove file"}
|
|
on:click={() => removeNewFile(i)}
|
|
>
|
|
<i class="ri-close-line" />
|
|
</button>
|
|
</div>
|
|
</Draggable>
|
|
{/each}
|
|
|
|
<div class="list-item list-item-btn">
|
|
<input
|
|
bind:this={fileInput}
|
|
type="file"
|
|
class="hidden"
|
|
accept={field.options?.mimeTypes?.join(",") || null}
|
|
multiple={isMultiple}
|
|
on:change={() => {
|
|
for (let file of fileInput.files) {
|
|
uploadedFiles.push(file);
|
|
}
|
|
uploadedFiles = uploadedFiles;
|
|
fileInput.value = null; // reset
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="btn btn-transparent btn-sm btn-block"
|
|
disabled={maxReached}
|
|
on:click={() => fileInput?.click()}
|
|
>
|
|
<i class="ri-upload-cloud-line" />
|
|
<span class="txt">Upload new file</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Field>
|
|
</div>
|