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

315 lines
8.4 KiB
Svelte
Raw Normal View History

2024-05-17 15:02:45 +02:00
<script lang="ts">
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
2024-05-21 22:37:52 +02:00
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
2024-05-17 15:02:45 +02:00
import Sortable from 'sortablejs/Sortable';
import { dbUtils, fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
2024-05-22 16:05:31 +02:00
import { get, type Readable } from 'svelte/store';
2024-05-17 15:02:45 +02:00
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
2024-05-21 13:22:14 +02:00
import FileListNodeLabel from './FileListNodeLabel.svelte';
import { ListLevel, ListTrackItem, 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>
| ReadonlyArray<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-05-22 16:05:31 +02:00
let elements: { [id: string | number]: 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
: 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-20 14:32:52 +02:00
let pull: Record<string, string[]> = {
file: ['file', 'track'],
track: ['file', 'track'],
segment: ['file', 'track', 'segment'],
waypoint: ['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
function onSelectChange() {
2024-05-24 13:16:41 +02:00
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')
);
2024-05-24 13:16:41 +02:00
});
return $selection;
2024-05-21 13:22:14 +02:00
});
2024-05-24 13:16:41 +02:00
}
2024-05-21 13:22:14 +02:00
}
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-23 11:21:57 +02:00
if ($fileOrder.length !== $fileObservers.size) {
// Files were added or removed
2024-05-23 12:57:24 +02:00
fileOrder.update((order) => {
for (let i = 0; i < order.length; ) {
if (!$fileObservers.has(order[i])) {
order.splice(i, 1);
} else {
i++;
}
}
for (let id of $fileObservers.keys()) {
if (!order.includes(id)) {
order.push(id);
}
}
return order;
});
2024-05-23 11:21:57 +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-05-17 15:02:45 +02:00
onMount(() => {
sortable = Sortable.create(container, {
2024-05-20 14:32:52 +02:00
group: {
name: sortableLevel
},
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,
onSelect: onSelectChange,
onDeselect: onSelectChange,
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);
return;
}
for (let i = 0; i < newFileOrder.length; i++) {
if (newFileOrder[i] !== get(fileOrder)[i]) {
fileOrder.set(newFileOrder);
return;
}
}
} else {
let fromItem = Sortable.get(e.from)._item;
let toItem = Sortable.get(e.to)._item;
let oldIndices =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
let newIndices =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
oldIndices.sort((a, b) => a - b);
newIndices.sort((a, b) => a - b);
let oldItems = oldIndices.map((i) => item.extend(i));
let newItems = newIndices.map((i) => item.extend(i));
if (fromItem === toItem) {
if (sortableLevel === ListLevel.TRACK) {
dbUtils.applyToFile(item.getFileId(), (draft) =>
draft.moveTracks(oldIndices, newIndices[0])
);
} else if (item instanceof ListTrackItem) {
dbUtils.applyToFile(item.getFileId(), (draft) =>
draft.moveTrackSegments(item.getTrackIndex(), oldIndices, newIndices[0])
);
} else if (sortableLevel === ListLevel.WAYPOINT) {
dbUtils.applyToFile(item.getFileId(), (draft) =>
draft.moveWaypoints(oldIndices, newIndices[0])
);
}
selection.update(($selection) => {
$selection.clear();
newItems.forEach((newItem) => {
console.log('newItem', newItem);
$selection.set(newItem, true);
});
return $selection;
});
} else if (item === toItem) {
// Move between lists
console.log('Move between lists');
}
2024-05-21 22:37:52 +02:00
}
}
2024-05-17 15:02:45 +02:00
});
Object.defineProperty(sortable, '_item', {
value: item
});
2024-05-24 13:16:41 +02:00
selection.set(get(selection));
2024-05-17 15:02:45 +02:00
});
2024-05-23 11:21:57 +02:00
$: if ($fileOrder) {
2024-05-21 22:37:52 +02:00
syncFileOrder();
}
afterUpdate(() => {
2024-05-23 16:35:20 +02:00
syncFileOrder();
2024-05-24 13:16:41 +02:00
if (sortableLevel === ListLevel.FILE) {
2024-05-23 11:21:57 +02:00
Object.keys(elements).forEach((fileId) => {
if (!get(fileObservers).has(fileId)) {
delete elements[fileId];
}
});
2024-05-24 13:16:41 +02:00
} else if (sortableLevel === ListLevel.WAYPOINTS) {
2024-05-23 16:35:20 +02:00
if (node.wpt.length === 0) {
delete elements['waypoints'];
}
} else {
Object.keys(elements).forEach((index) => {
if ((node instanceof GPXFile || node instanceof Track) && node.children.length <= index) {
delete elements[index];
} else if (Array.isArray(node) && node.length <= index) {
delete elements[index];
}
});
2024-05-23 11:21:57 +02:00
}
if (sortableLevel !== ListLevel.FILE) {
sortable.sort(Object.keys(elements));
}
2024-05-21 22:37:52 +02:00
});
2024-05-22 16:05:31 +02:00
const unsubscribe = selection.subscribe(($selection) => {
2024-05-24 13:16:41 +02:00
let changed = getChangedIds();
for (let id of changed) {
let element = elements[id];
if (element) {
if ($selection.has(item.extend(id))) {
Sortable.utils.select(element);
2024-06-03 16:06:14 +02:00
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
2024-05-24 13:16:41 +02:00
} else {
Sortable.utils.deselect(element);
}
}
}
});
function getChangedIds() {
let changed: (string | number)[] = [];
2024-05-22 16:05:31 +02:00
Object.entries(elements).forEach(([id, element]) => {
2024-05-24 13:16:41 +02:00
if (element === null) {
return;
}
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-05-21 13:22:14 +02:00
onDestroy(() => {
unsubscribe();
});
2024-05-17 15:02:45 +02:00
</script>
<div
bind:this={container}
class="sortable {orientation} flex {orientation === 'vertical' ? 'flex-col' : 'flex-row gap-1'}"
>
2024-05-17 15:02:45 +02:00
{#if node instanceof Map}
2024-05-21 13:22:14 +02:00
{#each node as [fileId, file]}
2024-05-22 16:05:31 +02:00
<div bind:this={elements[fileId]} data-id={fileId}>
<FileListNodeStore {file} />
2024-05-17 15:02:45 +02:00
</div>
{/each}
{:else if node instanceof GPXFile}
2024-05-21 13:22:14 +02:00
{#if waypointRoot}
{#if node.wpt.length > 0}
<div bind:this={elements['waypoints']} data-id="waypoints">
2024-05-22 16:05:31 +02:00
<FileListNode node={node.wpt} item={item.extend('waypoints')} />
2024-05-21 13:22:14 +02:00
</div>
{/if}
{:else}
{#each node.children as child, i}
<div bind:this={elements[i]} 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}
{#each node.children as child, i}
<div bind:this={elements[i]} data-id={i} class="ml-1">
2024-05-24 13:16:41 +02:00
<FileListNodeLabel item={item.extend(i)} label={`${$_('gpx.segment')} ${i + 1}`} />
2024-05-17 15:02:45 +02:00
</div>
{/each}
{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
{#each node as wpt, i}
<div bind:this={elements[i]} data-id={i} class="ml-1">
2024-05-24 13:16:41 +02:00
<FileListNodeLabel
item={item.extend(i)}
label={wpt.name ?? `${$_('gpx.waypoint')} ${i + 1}`}
/>
2024-05-17 15:02:45 +02:00
</div>
{/each}
{/if}
</div>
2024-05-21 13:22:14 +02:00
{#if node instanceof GPXFile}
{#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>