Files
gpx.studio/website/src/lib/components/file-list/FileListNodeContent.svelte

355 lines
8.7 KiB
Svelte
Raw Normal View History

2024-06-05 21:08:01 +02:00
<script lang="ts" context="module">
let dragging: Writable<ListLevel | null> = writable(null);
2024-06-05 23:37:55 +02:00
let updating = false;
2024-06-05 21:08:01 +02:00
</script>
2024-05-17 15:02:45 +02:00
<script lang="ts">
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
2024-07-03 22:42:08 +02:00
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
2024-05-17 15:02:45 +02:00
import Sortable from 'sortablejs/Sortable';
2024-06-05 23:37:55 +02:00
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
2024-06-05 21:08:01 +02:00
import { get, writable, type Readable, type Writable } from 'svelte/store';
2024-05-17 15:02:45 +02:00
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
2024-07-02 20:04:17 +02:00
import {
ListFileItem,
ListLevel,
ListRootItem,
ListWaypointsItem,
allowedMoves,
moveItems,
type ListItem
} from './FileList';
2024-05-22 16:05:31 +02:00
import { selection } from './Selection';
2024-05-24 13:16:41 +02:00
import { _ } from 'svelte-i18n';
2024-05-17 15:02:45 +02:00
export let node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
2024-06-18 15:32:54 +02:00
| Readonly<Waypoint>;
2024-05-22 16:05:31 +02:00
export let item: ListItem;
2024-05-21 13:22:14 +02:00
export let waypointRoot: boolean = false;
2024-05-17 15:02:45 +02:00
let container: HTMLElement;
2024-06-05 17:19:03 +02:00
let elements: { [id: string]: HTMLElement } = {};
2024-05-24 13:16:41 +02:00
let sortableLevel: ListLevel =
2024-05-17 15:02:45 +02:00
node instanceof Map
2024-05-24 13:16:41 +02:00
? ListLevel.FILE
2024-05-17 15:02:45 +02:00
: node instanceof GPXFile
2024-05-21 13:22:14 +02:00
? waypointRoot
2024-05-24 13:16:41 +02:00
? ListLevel.WAYPOINTS
2024-07-02 20:04:17 +02:00
: item instanceof ListWaypointsItem
? ListLevel.WAYPOINT
: ListLevel.TRACK
2024-05-17 15:02:45 +02:00
: node instanceof Track
2024-05-24 13:16:41 +02:00
? ListLevel.SEGMENT
: ListLevel.WAYPOINT;
2024-05-17 15:02:45 +02:00
let sortable: Sortable;
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
2024-05-21 13:22:14 +02:00
2024-07-03 22:42:08 +02:00
let destroyed = false;
let lastUpdateStart = 0;
2024-06-12 13:18:15 +02:00
function updateToSelection(e) {
2024-07-03 22:42:08 +02:00
if (destroyed) {
return;
}
lastUpdateStart = Date.now();
setTimeout(() => {
if (Date.now() - lastUpdateStart >= 40) {
if (updating) {
return;
2024-06-12 13:18:15 +02:00
}
updating = true;
// Sortable updates selection
let changed = getChangedIds();
if (changed.length > 0) {
selection.update(($selection) => {
$selection.clear();
Object.entries(elements).forEach(([id, element]) => {
$selection.set(
item.extend(getRealId(id)),
element.classList.contains('sortable-selected')
);
});
if (
e.originalEvent &&
$selection.size > 1 &&
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey)
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(item.extend(getRealId(changed[0])), true);
}
return $selection;
});
}
updating = false;
}
}, 50);
2024-05-21 13:22:14 +02:00
}
2024-06-05 23:37:55 +02:00
function updateFromSelection() {
2024-07-03 22:42:08 +02:00
if (destroyed || updating) {
return;
}
2024-06-05 23:37:55 +02:00
updating = true;
2024-06-05 17:19:03 +02:00
// Selection updates sortable
let changed = getChangedIds();
for (let id of changed) {
let element = elements[id];
if (element) {
if ($selection.has(item.extend(id))) {
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} else {
Sortable.utils.deselect(element);
}
}
}
2024-06-05 23:37:55 +02:00
updating = false;
2024-06-05 17:19:03 +02:00
}
$: if ($selection) {
updateFromSelection();
}
2024-05-23 11:21:57 +02:00
const { fileOrder } = settings;
2024-05-21 22:37:52 +02:00
function syncFileOrder() {
2024-05-24 13:16:41 +02:00
if (!sortable || sortableLevel !== ListLevel.FILE) {
2024-05-21 22:37:52 +02:00
return;
}
2024-05-22 16:05:31 +02:00
2024-05-21 22:37:52 +02:00
const currentOrder = sortable.toArray();
if (currentOrder.length !== $fileOrder.length) {
sortable.sort($fileOrder);
} else {
for (let i = 0; i < currentOrder.length; i++) {
if (currentOrder[i] !== $fileOrder[i]) {
sortable.sort($fileOrder);
break;
}
}
}
}
2024-06-05 17:19:03 +02:00
$: if ($fileOrder) {
syncFileOrder();
}
function createSortable() {
2024-05-17 15:02:45 +02:00
sortable = Sortable.create(container, {
2024-05-20 14:32:52 +02:00
group: {
2024-06-05 21:08:01 +02:00
name: sortableLevel,
2024-06-20 15:18:21 +02:00
pull: allowedMoves[sortableLevel],
2024-06-05 21:08:01 +02:00
put: true
2024-05-20 14:32:52 +02:00
},
direction: orientation,
2024-05-17 15:02:45 +02:00
forceAutoScrollFallback: true,
multiDrag: true,
multiDragKey: 'Meta',
2024-05-21 13:22:14 +02:00
avoidImplicitDeselect: true,
2024-06-05 17:19:03 +02:00
onSelect: updateToSelection,
onDeselect: updateToSelection,
2024-06-05 21:08:01 +02:00
onStart: () => {
dragging.set(sortableLevel);
},
onEnd: () => {
dragging.set(null);
},
onSort: (e) => {
if (sortableLevel === ListLevel.FILE) {
let newFileOrder = sortable.toArray();
if (newFileOrder.length !== get(fileOrder).length) {
2024-05-21 22:37:52 +02:00
fileOrder.set(newFileOrder);
2024-06-05 21:08:01 +02:00
} else {
for (let i = 0; i < newFileOrder.length; i++) {
if (newFileOrder[i] !== get(fileOrder)[i]) {
fileOrder.set(newFileOrder);
break;
}
}
2024-05-21 22:37:52 +02:00
}
2024-06-05 21:08:01 +02:00
}
2024-06-05 21:08:01 +02:00
let fromItem = Sortable.get(e.from)._item;
let toItem = Sortable.get(e.to)._item;
if (item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (Sortable.get(e.from)._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
2024-06-05 21:08:01 +02:00
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (Sortable.get(e.to)._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
newIndices.sort((a, b) => a - b);
2024-06-05 21:08:01 +02:00
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
2024-06-05 21:36:24 +02:00
toItems = newIndices.map((i, index) => {
$fileOrder.splice(i, 0, newFileIds[index]);
return item.extend(newFileIds[index]);
});
2024-06-04 16:11:47 +02:00
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
2024-06-05 21:08:01 +02:00
moveItems(fromItem, toItem, fromItems, toItems);
2024-05-21 22:37:52 +02:00
}
}
2024-05-17 15:02:45 +02:00
});
Object.defineProperty(sortable, '_item', {
2024-06-04 16:11:47 +02:00
value: item,
writable: true
});
2024-06-05 21:08:01 +02:00
Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot,
writable: true
});
2024-05-21 22:37:52 +02:00
}
2024-06-05 17:19:03 +02:00
onMount(() => {
createSortable();
2024-07-03 22:42:08 +02:00
destroyed = false;
2024-06-05 17:19:03 +02:00
});
2024-05-21 22:37:52 +02:00
afterUpdate(() => {
2024-06-05 17:19:03 +02:00
elements = {};
2024-06-05 21:36:24 +02:00
container.childNodes.forEach((element) => {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
2024-06-05 17:19:03 +02:00
if (attr) {
2024-06-05 21:36:24 +02:00
if (node instanceof Map && !node.has(attr)) {
element.remove();
} else {
elements[attr] = element;
}
2024-05-23 11:21:57 +02:00
}
2024-05-23 16:35:20 +02:00
}
2024-06-05 15:20:28 +02:00
});
2024-05-21 22:37:52 +02:00
2024-06-05 17:19:03 +02:00
syncFileOrder();
2024-06-05 23:37:55 +02:00
updateFromSelection();
2024-05-24 13:16:41 +02:00
});
2024-07-03 22:42:08 +02:00
onDestroy(() => {
destroyed = true;
});
2024-05-24 13:16:41 +02:00
function getChangedIds() {
let changed: (string | number)[] = [];
2024-05-22 16:05:31 +02:00
Object.entries(elements).forEach(([id, element]) => {
let realId = getRealId(id);
2024-05-24 13:16:41 +02:00
let realItem = item.extend(realId);
let inSelection = get(selection).has(realItem);
2024-05-22 16:05:31 +02:00
let isSelected = element.classList.contains('sortable-selected');
2024-05-24 13:16:41 +02:00
if (inSelection !== isSelected) {
changed.push(realId);
2024-05-21 13:22:14 +02:00
}
});
2024-05-24 13:16:41 +02:00
return changed;
}
2024-05-21 13:22:14 +02:00
function getRealId(id: string | number) {
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
? id
: parseInt(id);
}
2024-06-05 21:08:01 +02:00
2024-06-20 15:18:21 +02:00
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
2024-05-17 15:02:45 +02:00
</script>
<div
bind:this={container}
2024-06-05 21:08:01 +02:00
class="sortable {orientation} flex {orientation === 'vertical'
? 'flex-col'
2024-06-05 21:36:24 +02:00
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
>
2024-05-17 15:02:45 +02:00
{#if node instanceof Map}
2024-06-05 14:51:32 +02:00
{#each node as [fileId, file] (fileId)}
2024-06-05 17:19:03 +02:00
<div data-id={fileId}>
<FileListNodeStore {file} />
2024-05-17 15:02:45 +02:00
</div>
{/each}
{:else if node instanceof GPXFile}
2024-07-02 20:04:17 +02:00
{#if item instanceof ListWaypointsItem}
{#each node.wpt as wpt, i (wpt)}
<div data-id={i} class="ml-1">
<FileListNode node={wpt} item={item.extend(i)} />
</div>
{/each}
{:else if waypointRoot}
2024-05-21 13:22:14 +02:00
{#if node.wpt.length > 0}
2024-06-05 17:19:03 +02:00
<div data-id="waypoints">
2024-07-02 20:04:17 +02:00
<FileListNode {node} item={item.extend('waypoints')} />
2024-05-21 13:22:14 +02:00
</div>
{/if}
{:else}
2024-06-05 14:51:32 +02:00
{#each node.children as child, i (child)}
2024-06-05 17:19:03 +02:00
<div data-id={i}>
2024-05-22 16:05:31 +02:00
<FileListNode node={child} item={item.extend(i)} />
2024-05-21 13:22:14 +02:00
</div>
{/each}
2024-05-17 15:02:45 +02:00
{/if}
{:else if node instanceof Track}
2024-06-05 14:51:32 +02:00
{#each node.children as child, i (child)}
2024-06-05 17:19:03 +02:00
<div data-id={i} class="ml-1">
2024-06-05 14:51:32 +02:00
<FileListNode node={child} item={item.extend(i)} />
2024-05-17 15:02:45 +02:00
</div>
{/each}
{/if}
</div>
2024-07-02 20:04:17 +02:00
{#if node instanceof GPXFile && item instanceof ListFileItem}
2024-05-21 13:22:14 +02:00
{#if !waypointRoot}
2024-05-22 16:05:31 +02:00
<svelte:self {node} {item} waypointRoot={true} />
2024-05-21 13:22:14 +02:00
{/if}
{/if}
2024-05-17 15:02:45 +02:00
<style lang="postcss">
2024-05-21 13:22:14 +02:00
.sortable > div {
@apply rounded-md;
@apply h-fit;
@apply leading-none;
}
.vertical :global(.sortable-selected) {
2024-05-17 15:02:45 +02:00
@apply bg-accent;
}
.horizontal :global(button) {
@apply bg-accent;
@apply hover:bg-background;
}
.horizontal :global(.sortable-selected button) {
@apply bg-background;
}
2024-05-17 15:02:45 +02:00
</style>