mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
fix ctrl+click on tab, relates to #91
This commit is contained in:
@@ -1,26 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { isMac, isSafari } from '$lib/utils';
|
||||||
import { _ } from 'svelte-i18n';
|
import { onMount } from 'svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let key: string;
|
export let key: string;
|
||||||
export let shift: boolean = false;
|
export let shift: boolean = false;
|
||||||
export let ctrl: boolean = false;
|
export let ctrl: boolean = false;
|
||||||
export let click: boolean = false;
|
export let click: boolean = false;
|
||||||
|
|
||||||
let isMac = false;
|
let mac = false;
|
||||||
let isSafari = false;
|
let safari = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
mac = isMac();
|
||||||
isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
safari = isSafari();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
||||||
>
|
>
|
||||||
<span>{shift ? '⇧' : ''}</span>
|
<span>{shift ? '⇧' : ''}</span>
|
||||||
<span>{ctrl ? (isMac && !isSafari ? '⌘' : $_('menu.ctrl') + '+') : ''}</span>
|
<span>{ctrl ? (mac && !safari ? '⌘' : $_('menu.ctrl') + '+') : ''}</span>
|
||||||
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
||||||
<span>{click ? $_('menu.click') : ''}</span>
|
<span>{click ? $_('menu.click') : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,364 +1,374 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
let dragging: Writable<ListLevel | null> = writable(null);
|
let dragging: Writable<ListLevel | null> = writable(null);
|
||||||
|
|
||||||
let updating = false;
|
let updating = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListLevel,
|
ListLevel,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
ListWaypointsItem,
|
ListWaypointsItem,
|
||||||
allowedMoves,
|
allowedMoves,
|
||||||
moveItems,
|
moveItems,
|
||||||
type ListItem
|
type ListItem
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { _ } from 'svelte-i18n';
|
import { isMac } from '$lib/utils';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||||
| GPXTreeElement<AnyGPXTreeElement>
|
| GPXTreeElement<AnyGPXTreeElement>
|
||||||
| Waypoint;
|
| Waypoint;
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
export let waypointRoot: boolean = false;
|
export let waypointRoot: boolean = false;
|
||||||
|
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
let elements: { [id: string]: HTMLElement } = {};
|
let elements: { [id: string]: HTMLElement } = {};
|
||||||
let sortableLevel: ListLevel =
|
let sortableLevel: ListLevel =
|
||||||
node instanceof Map
|
node instanceof Map
|
||||||
? ListLevel.FILE
|
? ListLevel.FILE
|
||||||
: node instanceof GPXFile
|
: node instanceof GPXFile
|
||||||
? waypointRoot
|
? waypointRoot
|
||||||
? ListLevel.WAYPOINTS
|
? ListLevel.WAYPOINTS
|
||||||
: item instanceof ListWaypointsItem
|
: item instanceof ListWaypointsItem
|
||||||
? ListLevel.WAYPOINT
|
? ListLevel.WAYPOINT
|
||||||
: ListLevel.TRACK
|
: ListLevel.TRACK
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? ListLevel.SEGMENT
|
? ListLevel.SEGMENT
|
||||||
: ListLevel.WAYPOINT;
|
: ListLevel.WAYPOINT;
|
||||||
let sortable: Sortable;
|
let sortable: Sortable;
|
||||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||||
|
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
let lastUpdateStart = 0;
|
let lastUpdateStart = 0;
|
||||||
function updateToSelection(e) {
|
function updateToSelection(e) {
|
||||||
if (destroyed) {
|
if (destroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastUpdateStart = Date.now();
|
lastUpdateStart = Date.now();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (Date.now() - lastUpdateStart >= 40) {
|
if (Date.now() - lastUpdateStart >= 40) {
|
||||||
if (updating) {
|
if (updating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updating = true;
|
updating = true;
|
||||||
// Sortable updates selection
|
// Sortable updates selection
|
||||||
let changed = getChangedIds();
|
let changed = getChangedIds();
|
||||||
if (changed.length > 0) {
|
if (changed.length > 0) {
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
Object.entries(elements).forEach(([id, element]) => {
|
Object.entries(elements).forEach(([id, element]) => {
|
||||||
$selection.set(
|
$selection.set(
|
||||||
item.extend(getRealId(id)),
|
item.extend(getRealId(id)),
|
||||||
element.classList.contains('sortable-selected')
|
element.classList.contains('sortable-selected')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
e.originalEvent &&
|
e.originalEvent &&
|
||||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
|
!(
|
||||||
($selection.size > 1 || !$selection.has(item.extend(getRealId(changed[0]))))
|
e.originalEvent.ctrlKey ||
|
||||||
) {
|
e.originalEvent.metaKey ||
|
||||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
e.originalEvent.shiftKey
|
||||||
$selection.clear();
|
) &&
|
||||||
$selection.set(item.extend(getRealId(changed[0])), true);
|
($selection.size > 1 ||
|
||||||
}
|
!$selection.has(item.extend(getRealId(changed[0]))))
|
||||||
|
) {
|
||||||
|
// 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;
|
return $selection;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updating = false;
|
updating = false;
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromSelection() {
|
function updateFromSelection() {
|
||||||
if (destroyed || updating) {
|
if (destroyed || updating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updating = true;
|
updating = true;
|
||||||
// Selection updates sortable
|
// Selection updates sortable
|
||||||
let changed = getChangedIds();
|
let changed = getChangedIds();
|
||||||
for (let id of changed) {
|
for (let id of changed) {
|
||||||
let element = elements[id];
|
let element = elements[id];
|
||||||
if (element) {
|
if (element) {
|
||||||
if ($selection.has(item.extend(id))) {
|
if ($selection.has(item.extend(id))) {
|
||||||
Sortable.utils.select(element);
|
Sortable.utils.select(element);
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'nearest'
|
block: 'nearest'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Sortable.utils.deselect(element);
|
Sortable.utils.deselect(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updating = false;
|
updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selection) {
|
$: if ($selection) {
|
||||||
updateFromSelection();
|
updateFromSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fileOrder } = settings;
|
const { fileOrder } = settings;
|
||||||
function syncFileOrder() {
|
function syncFileOrder() {
|
||||||
if (!sortable || sortableLevel !== ListLevel.FILE) {
|
if (!sortable || sortableLevel !== ListLevel.FILE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentOrder = sortable.toArray();
|
const currentOrder = sortable.toArray();
|
||||||
if (currentOrder.length !== $fileOrder.length) {
|
if (currentOrder.length !== $fileOrder.length) {
|
||||||
sortable.sort($fileOrder);
|
sortable.sort($fileOrder);
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < currentOrder.length; i++) {
|
for (let i = 0; i < currentOrder.length; i++) {
|
||||||
if (currentOrder[i] !== $fileOrder[i]) {
|
if (currentOrder[i] !== $fileOrder[i]) {
|
||||||
sortable.sort($fileOrder);
|
sortable.sort($fileOrder);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($fileOrder) {
|
$: if ($fileOrder) {
|
||||||
syncFileOrder();
|
syncFileOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSortable() {
|
function createSortable() {
|
||||||
sortable = Sortable.create(container, {
|
sortable = Sortable.create(container, {
|
||||||
group: {
|
group: {
|
||||||
name: sortableLevel,
|
name: sortableLevel,
|
||||||
pull: allowedMoves[sortableLevel],
|
pull: allowedMoves[sortableLevel],
|
||||||
put: true
|
put: true
|
||||||
},
|
},
|
||||||
direction: orientation,
|
direction: orientation,
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
multiDrag: true,
|
multiDrag: true,
|
||||||
multiDragKey: 'Meta',
|
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
|
||||||
avoidImplicitDeselect: true,
|
avoidImplicitDeselect: true,
|
||||||
onSelect: updateToSelection,
|
onSelect: updateToSelection,
|
||||||
onDeselect: updateToSelection,
|
onDeselect: updateToSelection,
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
dragging.set(sortableLevel);
|
dragging.set(sortableLevel);
|
||||||
},
|
},
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
dragging.set(null);
|
dragging.set(null);
|
||||||
},
|
},
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
if (sortableLevel === ListLevel.FILE) {
|
if (sortableLevel === ListLevel.FILE) {
|
||||||
let newFileOrder = sortable.toArray();
|
let newFileOrder = sortable.toArray();
|
||||||
if (newFileOrder.length !== get(fileOrder).length) {
|
if (newFileOrder.length !== get(fileOrder).length) {
|
||||||
fileOrder.set(newFileOrder);
|
fileOrder.set(newFileOrder);
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < newFileOrder.length; i++) {
|
for (let i = 0; i < newFileOrder.length; i++) {
|
||||||
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
||||||
fileOrder.set(newFileOrder);
|
fileOrder.set(newFileOrder);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fromItem = Sortable.get(e.from)._item;
|
let fromItem = Sortable.get(e.from)._item;
|
||||||
let toItem = Sortable.get(e.to)._item;
|
let toItem = Sortable.get(e.to)._item;
|
||||||
|
|
||||||
if (item === toItem && !(fromItem instanceof ListRootItem)) {
|
if (item === toItem && !(fromItem instanceof ListRootItem)) {
|
||||||
// Event is triggered on source and destination list, only handle it once
|
// Event is triggered on source and destination list, only handle it once
|
||||||
let fromItems = [];
|
let fromItems = [];
|
||||||
let toItems = [];
|
let toItems = [];
|
||||||
|
|
||||||
if (Sortable.get(e.from)._waypointRoot) {
|
if (Sortable.get(e.from)._waypointRoot) {
|
||||||
fromItems = [fromItem.extend('waypoints')];
|
fromItems = [fromItem.extend('waypoints')];
|
||||||
} else {
|
} else {
|
||||||
let oldIndices: number[] =
|
let oldIndices: number[] =
|
||||||
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
e.oldIndicies.length > 0
|
||||||
oldIndices = oldIndices.filter((i) => i >= 0);
|
? e.oldIndicies.map((i) => i.index)
|
||||||
oldIndices.sort((a, b) => a - b);
|
: [e.oldIndex];
|
||||||
|
oldIndices = oldIndices.filter((i) => i >= 0);
|
||||||
|
oldIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
fromItems = oldIndices.map((i) => fromItem.extend(i));
|
fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
|
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
|
||||||
toItems = [toItem.extend('waypoints')];
|
toItems = [toItem.extend('waypoints')];
|
||||||
} else {
|
} else {
|
||||||
if (Sortable.get(e.to)._waypointRoot) {
|
if (Sortable.get(e.to)._waypointRoot) {
|
||||||
toItem = toItem.extend('waypoints');
|
toItem = toItem.extend('waypoints');
|
||||||
}
|
}
|
||||||
|
|
||||||
let newIndices: number[] =
|
let newIndices: number[] =
|
||||||
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
e.newIndicies.length > 0
|
||||||
newIndices = newIndices.filter((i) => i >= 0);
|
? e.newIndicies.map((i) => i.index)
|
||||||
newIndices.sort((a, b) => a - b);
|
: [e.newIndex];
|
||||||
|
newIndices = newIndices.filter((i) => i >= 0);
|
||||||
|
newIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
if (toItem instanceof ListRootItem) {
|
if (toItem instanceof ListRootItem) {
|
||||||
let newFileIds = getFileIds(newIndices.length);
|
let newFileIds = getFileIds(newIndices.length);
|
||||||
toItems = newIndices.map((i, index) => {
|
toItems = newIndices.map((i, index) => {
|
||||||
$fileOrder.splice(i, 0, newFileIds[index]);
|
$fileOrder.splice(i, 0, newFileIds[index]);
|
||||||
return item.extend(newFileIds[index]);
|
return item.extend(newFileIds[index]);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toItems = newIndices.map((i) => toItem.extend(i));
|
toItems = newIndices.map((i) => toItem.extend(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveItems(fromItem, toItem, fromItems, toItems);
|
moveItems(fromItem, toItem, fromItems, toItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.defineProperty(sortable, '_item', {
|
Object.defineProperty(sortable, '_item', {
|
||||||
value: item,
|
value: item,
|
||||||
writable: true
|
writable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(sortable, '_waypointRoot', {
|
Object.defineProperty(sortable, '_waypointRoot', {
|
||||||
value: waypointRoot,
|
value: waypointRoot,
|
||||||
writable: true
|
writable: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
createSortable();
|
createSortable();
|
||||||
destroyed = false;
|
destroyed = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
elements = {};
|
elements = {};
|
||||||
container.childNodes.forEach((element) => {
|
container.childNodes.forEach((element) => {
|
||||||
if (element instanceof HTMLElement) {
|
if (element instanceof HTMLElement) {
|
||||||
let attr = element.getAttribute('data-id');
|
let attr = element.getAttribute('data-id');
|
||||||
if (attr) {
|
if (attr) {
|
||||||
if (node instanceof Map && !node.has(attr)) {
|
if (node instanceof Map && !node.has(attr)) {
|
||||||
element.remove();
|
element.remove();
|
||||||
} else {
|
} else {
|
||||||
elements[attr] = element;
|
elements[attr] = element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
syncFileOrder();
|
syncFileOrder();
|
||||||
updateFromSelection();
|
updateFromSelection();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getChangedIds() {
|
function getChangedIds() {
|
||||||
let changed: (string | number)[] = [];
|
let changed: (string | number)[] = [];
|
||||||
Object.entries(elements).forEach(([id, element]) => {
|
Object.entries(elements).forEach(([id, element]) => {
|
||||||
let realId = getRealId(id);
|
let realId = getRealId(id);
|
||||||
let realItem = item.extend(realId);
|
let realItem = item.extend(realId);
|
||||||
let inSelection = get(selection).has(realItem);
|
let inSelection = get(selection).has(realItem);
|
||||||
let isSelected = element.classList.contains('sortable-selected');
|
let isSelected = element.classList.contains('sortable-selected');
|
||||||
if (inSelection !== isSelected) {
|
if (inSelection !== isSelected) {
|
||||||
changed.push(realId);
|
changed.push(realId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRealId(id: string | number) {
|
function getRealId(id: string | number) {
|
||||||
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
||||||
? id
|
? id
|
||||||
: parseInt(id);
|
: parseInt(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
|
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="sortable {orientation} flex {orientation === 'vertical'
|
class="sortable {orientation} flex {orientation === 'vertical'
|
||||||
? 'flex-col'
|
? 'flex-col'
|
||||||
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
|
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
|
||||||
>
|
>
|
||||||
{#if node instanceof Map}
|
{#if node instanceof Map}
|
||||||
{#each node as [fileId, file] (fileId)}
|
{#each node as [fileId, file] (fileId)}
|
||||||
<div data-id={fileId}>
|
<div data-id={fileId}>
|
||||||
<FileListNodeStore {file} />
|
<FileListNodeStore {file} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if node instanceof GPXFile}
|
{:else if node instanceof GPXFile}
|
||||||
{#if item instanceof ListWaypointsItem}
|
{#if item instanceof ListWaypointsItem}
|
||||||
{#each node.wpt as wpt, i (wpt)}
|
{#each node.wpt as wpt, i (wpt)}
|
||||||
<div data-id={i} class="ml-1">
|
<div data-id={i} class="ml-1">
|
||||||
<FileListNode node={wpt} item={item.extend(i)} />
|
<FileListNode node={wpt} item={item.extend(i)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if waypointRoot}
|
{:else if waypointRoot}
|
||||||
{#if node.wpt.length > 0}
|
{#if node.wpt.length > 0}
|
||||||
<div data-id="waypoints">
|
<div data-id="waypoints">
|
||||||
<FileListNode {node} item={item.extend('waypoints')} />
|
<FileListNode {node} item={item.extend('waypoints')} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#each node.children as child, i (child)}
|
{#each node.children as child, i (child)}
|
||||||
<div data-id={i}>
|
<div data-id={i}>
|
||||||
<FileListNode node={child} item={item.extend(i)} />
|
<FileListNode node={child} item={item.extend(i)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if node instanceof Track}
|
{:else if node instanceof Track}
|
||||||
{#each node.children as child, i (child)}
|
{#each node.children as child, i (child)}
|
||||||
<div data-id={i} class="ml-1">
|
<div data-id={i} class="ml-1">
|
||||||
<FileListNode node={child} item={item.extend(i)} />
|
<FileListNode node={child} item={item.extend(i)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if node instanceof GPXFile && item instanceof ListFileItem}
|
{#if node instanceof GPXFile && item instanceof ListFileItem}
|
||||||
{#if !waypointRoot}
|
{#if !waypointRoot}
|
||||||
<svelte:self {node} {item} waypointRoot={true} />
|
<svelte:self {node} {item} waypointRoot={true} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.sortable > div {
|
.sortable > div {
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
@apply h-fit;
|
@apply h-fit;
|
||||||
@apply leading-none;
|
@apply leading-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(button) {
|
.vertical :global(button) {
|
||||||
@apply hover:bg-muted;
|
@apply hover:bg-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(.sortable-selected button) {
|
.vertical :global(.sortable-selected button) {
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(.sortable-selected) {
|
.vertical :global(.sortable-selected) {
|
||||||
@apply bg-accent;
|
@apply bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(button) {
|
.horizontal :global(button) {
|
||||||
@apply bg-accent;
|
@apply bg-accent;
|
||||||
@apply hover:bg-muted;
|
@apply hover:bg-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.sortable-selected button) {
|
.horizontal :global(.sortable-selected button) {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -178,6 +178,14 @@ export function setScissorsCursor() {
|
|||||||
setCursor(scissorsCursor);
|
setCursor(scissorsCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMac() {
|
||||||
|
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSafari() {
|
||||||
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
export function getURLForLanguage(lang: string | null | undefined, path: string): string {
|
export function getURLForLanguage(lang: string | null | undefined, path: string): string {
|
||||||
let newPath = path.replace(base, '');
|
let newPath = path.replace(base, '');
|
||||||
let languageInPath = newPath.split('/')[1];
|
let languageInPath = newPath.split('/')[1];
|
||||||
|
Reference in New Issue
Block a user