buggy sortable file elements

This commit is contained in:
vcoppe
2024-06-04 16:11:47 +02:00
parent ac83e4bf77
commit 256996379a
5 changed files with 299 additions and 244 deletions

View File

@@ -176,43 +176,28 @@ export class GPXFile extends GPXTreeNode<Track>{
}
// Producers
replaceTracks(start: number, end: number, tracks: Track[]) {
return produce(this, (draft) => {
replaceTracks(start: number, end: number, tracks: Track[]): [GPXFile, Track[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk.splice(start, end - start + 1, ...tracks);
removed = trk.splice(start, end - start + 1, ...tracks);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
return [result, removed];
}
moveTracks(indices: number[], dest: number) {
return produce(this, (draft) => {
replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]): [GPXFile, TrackSegment[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
let tracks = indices.map((index) => trk[index]);
indices.sort((a, b) => b - a);
indices.forEach((index) => trk.splice(index, 1));
trk.splice(dest, 0, ...tracks);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk[trackIndex] = trk[trackIndex].replaceTrackSegments(start, end, segments);
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
}
moveTrackSegments(trackIndex: number, indices: number[], dest: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trk = og.trk.slice();
trk[trackIndex] = trk[trackIndex].moveTrackSegments(indices, dest);
let [result, rmv] = trk[trackIndex].replaceTrackSegments(start, end, segments);
trk[trackIndex] = result;
removed = rmv;
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
});
return [result, removed];
}
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
@@ -224,25 +209,15 @@ export class GPXFile extends GPXTreeNode<Track>{
});
}
replaceWaypoints(start: number, end: number, waypoints: Waypoint[]) {
return produce(this, (draft) => {
replaceWaypoints(start: number, end: number, waypoints: Waypoint[]): [GPXFile, Waypoint[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let wpt = og.wpt.slice();
wpt.splice(start, end - start + 1, ...waypoints);
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
});
}
moveWaypoints(indices: number[], dest: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let wpt = og.wpt.slice();
let waypoints = indices.map((index) => wpt[index]);
indices.sort((a, b) => b - a);
indices.forEach((index) => wpt.splice(index, 1));
wpt.splice(dest, 0, ...waypoints);
removed = wpt.splice(start, end - start + 1, ...waypoints);
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
});
return [result, removed];
}
reverse() {
@@ -350,25 +325,15 @@ export class Track extends GPXTreeNode<TrackSegment> {
}
// Producers
replaceTrackSegments(start: number, end: number, segments: TrackSegment[]) {
return produce(this, (draft) => {
replaceTrackSegments(start: number, end: number, segments: TrackSegment[]): [Track, TrackSegment[]] {
let removed = [];
let result = produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
trkseg.splice(start, end - start + 1, ...segments);
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
}
moveTrackSegments(indices: number[], dest: number) {
return produce(this, (draft) => {
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
let trkseg = og.trkseg.slice();
let segments = indices.map((index) => trkseg[index]);
indices.sort((a, b) => b - a);
indices.forEach((index) => trkseg.splice(index, 1));
trkseg.splice(dest, 0, ...segments);
removed = trkseg.splice(start, end - start + 1, ...segments);
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
});
return [result, removed];
}
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[]) {

View File

@@ -1,134 +1,8 @@
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection?: ListItem[]): ListItem[] {
if (selection === undefined) {
selection = [];
}
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
this.size -= this.children[id].size;
delete this.children[id];
}
};
import { dbUtils, fileObservers } from "$lib/db";
import { castDraft } from "immer";
import { Track, TrackSegment, Waypoint } from "gpx";
import { selection } from "./Selection";
import { get } from "svelte/store";
export enum ListLevel {
ROOT,
@@ -363,3 +237,85 @@ export class ListWaypointItem extends ListItem {
return this;
}
}
export function sortItems(items: ListItem[], reverse: boolean = false) {
items.sort((a, b) => {
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
return a.getTrackIndex() - b.getTrackIndex();
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
return a.getSegmentIndex() - b.getSegmentIndex();
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
return a.getWaypointIndex() - b.getWaypointIndex();
}
return a.level - b.level;
});
if (reverse) {
items.reverse();
}
}
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[]) {
sortItems(fromItems, true);
sortItems(toItems, false);
let toFileObserver = get(fileObservers).get(toParent.getFileId());
let first = true;
toFileObserver?.subscribe(() => { // Update selection when the target file has been updated
if (first) first = false;
else {
selection.update(($selection) => {
$selection.clear();
toItems.forEach((item) => {
$selection.set(item, true);
});
return $selection;
});
}
});
dbUtils.applyEachToFiles([fromParent.getFileId(), toParent.getFileId()], [
(file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
let newFile = file;
fromItems.forEach((item) => {
if (item instanceof ListTrackItem) {
let [result, removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
newFile = castDraft(result);
context.push(...removed);
} else if (item instanceof ListTrackSegmentItem) {
let [result, removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
newFile = castDraft(result);
context.push(...removed);
} else if (item instanceof ListWaypointsItem) {
let [result, removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
newFile = castDraft(result);
context.push(removed);
} else if (item instanceof ListWaypointItem) {
let [result, removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
newFile = castDraft(result);
context.push(...removed);
}
});
context.reverse();
return newFile;
},
(file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
let newFile = file;
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem && context[i] instanceof Track) {
let [result, _removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
newFile = castDraft(result);
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
let [result, _removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
newFile = castDraft(result);
} else if (item instanceof ListWaypointsItem && Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
let [result, _removed] = newFile.replaceWaypoints(0, -1, context[i]);
newFile = castDraft(result);
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
let [result, _removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
newFile = castDraft(result);
}
});
return newFile;
}
], []);
}

View File

@@ -2,12 +2,12 @@
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable';
import { dbUtils, fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
import { fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
import { get, type Readable } from 'svelte/store';
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
import FileListNodeLabel from './FileListNodeLabel.svelte';
import { ListLevel, ListTrackItem, type ListItem } from './FileList';
import { ListLevel, moveItems, type ListItem } from './FileList';
import { selection } from './Selection';
import { _ } from 'svelte-i18n';
@@ -123,47 +123,35 @@
} 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 (item === toItem) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
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])
);
if (waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
toItems = [toItem.extend('waypoints')];
} else {
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);
fromItems = oldIndices.map((i) => fromItem.extend(i));
toItems = newIndices.map((i) => toItem.extend(i));
}
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');
moveItems(fromItem, toItem, fromItems, toItems);
}
}
}
});
Object.defineProperty(sortable, '_item', {
value: item
value: item,
writable: true
});
selection.set(get(selection));
});

View File

@@ -1,7 +1,139 @@
import { get, writable } from "svelte/store";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, SelectionTreeType, type ListLevel } from "./FileList";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, type ListLevel, sortItems } from "./FileList";
import { fileObservers, settings } from "$lib/db";
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection?: ListItem[]): ListItem[] {
if (selection === undefined) {
selection = [];
}
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
this.size -= this.children[id].size;
delete this.children[id];
}
};
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
@@ -80,19 +212,7 @@ export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, l
});
if (items.length > 0) {
if (reverse) {
items.sort((a, b) => { // Process the items in reverse order to avoid index conflicts
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
return b.getTrackIndex() - a.getTrackIndex();
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
return b.getSegmentIndex() - a.getSegmentIndex();
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
return b.getWaypointIndex() - a.getWaypointIndex();
}
return b.level - a.level;
});
}
sortItems(items, reverse);
callback(fileId, level, items);
}
});

View File

@@ -291,6 +291,22 @@ function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>
return commitFileStateChange(newFileState, patch);
}
// Helper function to apply different callbacks to multiple files
function applyEachToFiles(fileIds: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], context?: any) {
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
fileIds.forEach((fileId, index) => {
let file = draft.get(fileId);
if (file) {
draft.set(fileId, castDraft(callbacks[index](file, context)));
}
});
});
storePatches(patch, inversePatch);
return commitFileStateChange(newFileState, patch);
}
const MAX_PATCHES = 100;
// Store the new patches in the database
async function storePatches(patch: Patch[], inversePatch: Patch[]) {
@@ -362,6 +378,9 @@ export const dbUtils = {
applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
applyToFiles(ids, callback);
},
applyEachToFiles: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], context?: any) => {
applyEachToFiles(ids, callbacks, context);
},
applyToSelection: (callback: (file: WritableDraft<AnyGPXTreeElement>) => AnyGPXTreeElement) => {
if (get(selection).size === 0) {
return;
@@ -408,18 +427,21 @@ export const dbUtils = {
} else if (level === ListLevel.TRACK) {
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
newFile = newFile.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
let [result, _removed] = newFile.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
newFile = result;
}
} else if (level === ListLevel.SEGMENT) {
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
newFile = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
newFile = result;
}
} else if (level === ListLevel.WAYPOINT) {
for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
newFile = newFile.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
let [result, _removed] = newFile.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
newFile = result;
}
}
draft.set(newFile._data.id, freeze(newFile));
@@ -470,20 +492,24 @@ export const dbUtils = {
if (level === ListLevel.TRACK) {
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
newFile = newFile.replaceTracks(trackIndex, trackIndex, []);
let [result, _removed] = newFile.replaceTracks(trackIndex, trackIndex, []);
newFile = result;
}
} else if (level === ListLevel.SEGMENT) {
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
newFile = newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
newFile = result;
}
} else if (level === ListLevel.WAYPOINTS) {
newFile = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
let [result, _removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
newFile = result;
} else if (level === ListLevel.WAYPOINT) {
for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
newFile = newFile.replaceWaypoints(waypointIndex, waypointIndex, []);
let [result, _removed] = newFile.replaceWaypoints(waypointIndex, waypointIndex, []);
newFile = result;
}
}
draft.set(newFile._data.id, freeze(newFile));