✨ Trip review panel, ⚡ LatLng parser to support more copy/paste format, 💄 Trip collapsible panels, 💄 Trip minor UI, 🐛 Fix select overflow on long text
This commit is contained in:
parent
5ae894c577
commit
c32a04c991
@ -1 +1 @@
|
||||
__version__ = "1.3.0"
|
||||
__version__ = "1.3.1"
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex md:hidden">
|
||||
<p-button (click)="menu.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
|
||||
<p-menu #menu [model]="menuItems" [popup]="true" />
|
||||
<p-button (click)="menuTripActions.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
|
||||
<p-menu #menuTripActions [model]="menuTripActionsItems" [popup]="true" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -62,8 +62,8 @@
|
||||
|
||||
@defer {
|
||||
@if (flattenedTripItems.length) {
|
||||
<p-table [value]="flattenedTripItems" styleClass="max-w-[85vw] md:max-w-full" rowGroupMode="rowspan"
|
||||
groupRowsBy="td_label">
|
||||
<p-table [value]="flattenedTripItems" class="print-striped-rows" styleClass="max-w-[85vw] md:max-w-full"
|
||||
rowGroupMode="rowspan" groupRowsBy="td_label">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>Day</th>
|
||||
@ -97,7 +97,7 @@
|
||||
</div>
|
||||
} @else {-}
|
||||
</td>
|
||||
<td class="max-w-20 truncate">{{ tripitem.comment || '-' }}</td>
|
||||
<td class="max-w-20 truncate print:whitespace-normal">{{ tripitem.comment || '-' }}</td>
|
||||
<td class="font-mono text-sm">
|
||||
<div class="max-w-20 print:max-w-full truncate">
|
||||
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
|
||||
@ -214,50 +214,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
} @else {
|
||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
||||
}
|
||||
<div class="z-10 p-4 shadow rounded-md w-full min-h-20 max-h-full overflow-y-auto">
|
||||
<div class="p-2 mb-2 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="font-semibold tracking-tight text-xl">Days</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span>
|
||||
<h1 class="font-semibold tracking-tight text-xl">Map</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} places</span>
|
||||
</div>
|
||||
|
||||
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
|
||||
<p-button icon="pi pi-refresh" [disabled]="!places.length" (click)="resetMapBounds()" text />
|
||||
</div>
|
||||
|
||||
<div class="max-h-[20vh] overflow-y-auto">
|
||||
@defer {
|
||||
@for (d of trip?.days; track d.id) {
|
||||
<div class="group flex items-center rounded-md justify-between h-10 px-4 py-2 hover:bg-gray-50">
|
||||
{{ d.label }}
|
||||
<div>
|
||||
<span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded-md group-hover:hidden">{{
|
||||
getDayStats(d).price || '-' }} {{ currency$ | async }}</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-sm me-2 px-2.5 py-0.5 rounded-md group-hover:hidden">{{
|
||||
getDayStats(d).places }}</span>
|
||||
</div>
|
||||
<div class="hidden group-hover:flex gap-2 items-center">
|
||||
<p-button icon="pi pi-trash" severity="danger" [disabled]="trip?.archived" (click)="deleteDay(d)" text />
|
||||
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editDay(d)" label="Edit" text />
|
||||
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem(d.id)" label="Item" text />
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
|
||||
}
|
||||
} @placeholder (minimum 0.4s) {
|
||||
<div class="h-16">
|
||||
<p-skeleton height="100%" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="map" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
|
||||
</div>
|
||||
|
||||
@if (!selectedItem) {
|
||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
||||
<div class="p-2 mb-2 flex justify-between items-center">
|
||||
<div>
|
||||
<div class="group relative">
|
||||
<h1 class="font-semibold tracking-tight text-xl">Places</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} places</span>
|
||||
|
||||
<div class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x">
|
||||
<p-button [icon]="collapsedTripPlaces ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
|
||||
(click)="collapsedTripPlaces = !collapsedTripPlaces" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
@ -270,6 +251,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!collapsedTripPlaces) {
|
||||
<div class="max-h-[25vh] overflow-y-auto">
|
||||
@defer {
|
||||
@for (p of places; track p.id) {
|
||||
@ -313,20 +295,104 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
||||
<div class="p-2 mb-2 flex justify-between items-center">
|
||||
<div class="group relative">
|
||||
<h1 class="font-semibold tracking-tight text-xl">Days</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span>
|
||||
|
||||
<div class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x">
|
||||
<p-button [icon]="collapsedTripDays ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
|
||||
(click)="collapsedTripDays = !collapsedTripDays" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
|
||||
</div>
|
||||
|
||||
@if (!collapsedTripDays) {
|
||||
<div class="max-h-[20vh] overflow-y-auto">
|
||||
@defer {
|
||||
@for (d of trip?.days; track d.id) {
|
||||
<div
|
||||
class="group flex items-center gap-4 rounded-md justify-between h-10 px-4 py-2 hover:bg-gray-50 w-full max-w-full">
|
||||
<div class="line-clamp-1">
|
||||
{{ d.label }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-none">
|
||||
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit group-hover:hidden">{{
|
||||
getDayStats(d).price || '-' }} {{ currency$ | async }}</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md group-hover:hidden">{{
|
||||
getDayStats(d).places }}</span>
|
||||
|
||||
<div class="flex md:hidden">
|
||||
<p-button (click)="selectedTripDayForMenu = d; menuTripDayActions.toggle($event)" severity="secondary"
|
||||
text icon="pi pi-ellipsis-h" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden group-hover:flex gap-2 items-center flex-none">
|
||||
<p-button icon="pi pi-trash" severity="danger" [disabled]="trip?.archived" (click)="deleteDay(d)" text />
|
||||
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editDay(d)" label="Edit" text />
|
||||
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem(d.id)" label="Item" text />
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
|
||||
}
|
||||
} @placeholder (minimum 0.4s) {
|
||||
<div class="h-16">
|
||||
<p-skeleton height="100%" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="z-10 p-4 shadow rounded-md w-full min-h-20 max-h-full overflow-y-auto">
|
||||
<div class="p-2 mb-2 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="font-semibold tracking-tight text-xl">Map</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} places</span>
|
||||
</div>
|
||||
|
||||
<p-button icon="pi pi-refresh" [disabled]="!places.length" (click)="resetMapBounds()" text />
|
||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
||||
<div class="group relative p-2 mb-2 flex flex-col items-start">
|
||||
<h1 class="font-semibold tracking-tight text-xl">Review</h1>
|
||||
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} pending/constraints</span>
|
||||
|
||||
<div class="bg-white rounded py-2 absolute top-1/2 -translate-y-1/2 left-0 hidden group-hover:block slide-x">
|
||||
<p-button [icon]="collapsedTripStatuses ? 'pi pi-chevron-down' : 'pi pi-chevron-up'" text
|
||||
(click)="collapsedTripStatuses = !collapsedTripStatuses" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
|
||||
@if (!collapsedTripStatuses) {
|
||||
<div class="max-h-[20vh] overflow-y-auto">
|
||||
@defer {
|
||||
@for (item of getReviewData; track item.id) {
|
||||
<div class="flex items-center gap-4 rounded-md justify-between h-10 px-4 py-2 w-full max-w-full">
|
||||
<div class="line-clamp-1">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-none">
|
||||
@if (item.status) {
|
||||
<span [style.background]="item.status.color+'1A'" [style.color]="item.status.color"
|
||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
||||
item.status.label }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="p-4 font-light text-gray-500">
|
||||
Nothing to review
|
||||
</p>
|
||||
}
|
||||
} @placeholder (minimum 0.4s) {
|
||||
<div class="h-16">
|
||||
<p-skeleton height="100%" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<p-menu #menuTripDayActions [model]="menuTripDayActionsItems" appendTo="body" [popup]="true" />
|
||||
@ -0,0 +1,10 @@
|
||||
@media print {
|
||||
.print-striped-rows tr:nth-child(even) {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.print-striped-rows tr:nth-child(even) td:first-child.truncate {
|
||||
//HACK: The "day" column is truncated
|
||||
background-color: white !important;
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,14 @@ export class TripComponent implements AfterViewInit {
|
||||
|
||||
places: PlaceWithUsage[] = [];
|
||||
flattenedTripItems: FlattenedTripItem[] = [];
|
||||
menuItems: MenuItem[] = [];
|
||||
menuTripActionsItems: MenuItem[] = [];
|
||||
|
||||
menuTripDayActionsItems: MenuItem[] = [];
|
||||
selectedTripDayForMenu: TripDay | undefined;
|
||||
|
||||
collapsedTripPlaces: boolean = false;
|
||||
collapsedTripDays: boolean = false;
|
||||
collapsedTripStatuses: boolean = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@ -81,7 +88,7 @@ export class TripComponent implements AfterViewInit {
|
||||
this.currency$ = this.utilsService.currency$;
|
||||
this.statuses = this.utilsService.statuses;
|
||||
|
||||
this.menuItems = [
|
||||
this.menuTripActionsItems = [
|
||||
{
|
||||
label: "Actions",
|
||||
items: [
|
||||
@ -112,6 +119,38 @@ export class TripComponent implements AfterViewInit {
|
||||
],
|
||||
},
|
||||
];
|
||||
this.menuTripDayActionsItems = [
|
||||
{
|
||||
label: "Actions",
|
||||
items: [
|
||||
{
|
||||
label: "Item",
|
||||
icon: "pi pi-plus",
|
||||
iconClass: "text-blue-500!",
|
||||
command: () => {
|
||||
this.addItem();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
icon: "pi pi-pencil",
|
||||
command: () => {
|
||||
if (!this.selectedTripDayForMenu) return;
|
||||
this.editDay(this.selectedTripDayForMenu);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: "pi pi-trash",
|
||||
iconClass: "text-red-500!",
|
||||
command: () => {
|
||||
if (!this.selectedTripDayForMenu) return;
|
||||
this.deleteDay(this.selectedTripDayForMenu);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
back() {
|
||||
@ -166,6 +205,20 @@ export class TripComponent implements AfterViewInit {
|
||||
return stats;
|
||||
}
|
||||
|
||||
get getReviewData(): (TripItem & { status: TripStatus })[] {
|
||||
if (!this.trip?.days) return [];
|
||||
|
||||
const data = this.trip!.days.map((day) =>
|
||||
day.items.filter((item) => item.status),
|
||||
).flat();
|
||||
if (!data.length) return [];
|
||||
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
status: this.statusToTripStatus(item.status as string),
|
||||
})) as (TripItem & { status: TripStatus })[];
|
||||
}
|
||||
|
||||
statusToTripStatus(status?: string): TripStatus | undefined {
|
||||
if (!status) return undefined;
|
||||
return this.statuses.find((s) => s.label == status) as TripStatus;
|
||||
@ -576,6 +629,7 @@ export class TripComponent implements AfterViewInit {
|
||||
this.trip?.days!,
|
||||
);
|
||||
}
|
||||
if (item.price) this.updateTotalPrice(item.price);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -21,15 +21,19 @@
|
||||
<label for="place">Place</label>
|
||||
</p-floatlabel>
|
||||
<p-inputgroup-addon>
|
||||
<p-button icon="pi pi-info-circle" [pTooltip]="placeInputTooltip" [escape]="false" tooltipEvent="focus"
|
||||
severity="secondary" class="h-full" />
|
||||
<p-button icon="pi pi-info-circle" tabindex="-1" [pTooltip]="placeInputTooltip" [escape]="false"
|
||||
tooltipEvent="focus" severity="secondary" class="h-full" />
|
||||
</p-inputgroup-addon>
|
||||
</p-inputgroup>
|
||||
|
||||
<p-floatlabel variant="in" class="col-span-2 md:col-span-1">
|
||||
<p-select [options]="(categories$ | async) || []" optionValue="id" optionLabel="name"
|
||||
[loading]="!(categories$ | async)?.length" inputId="category" id="category" formControlName="category"
|
||||
[checkmark]="true" class="w-full capitalize" fluid />
|
||||
[checkmark]="true" class="capitalize" fluid>
|
||||
<ng-template let-category #item>
|
||||
<div class="whitespace-normal">{{ category.name }}</div>
|
||||
</ng-template>
|
||||
</p-select>
|
||||
<label for="category">Category</label>
|
||||
</p-floatlabel>
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import { FocusTrapModule } from "primeng/focustrap";
|
||||
import { Category, Place } from "../../types/poi";
|
||||
import { CheckboxModule } from "primeng/checkbox";
|
||||
import { TooltipModule } from "primeng/tooltip";
|
||||
import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser";
|
||||
|
||||
@Component({
|
||||
selector: "app-place-create-modal",
|
||||
@ -114,30 +115,18 @@ export class PlaceCreateModalComponent {
|
||||
|
||||
this.placeForm.get("lat")?.valueChanges.subscribe({
|
||||
next: (value: string) => {
|
||||
if (/\-?\d+\.\d+,\s\-?\d+\.\d+/.test(value)) {
|
||||
let [lat, lng] = value.split(", ");
|
||||
const latLength = lat.split(".")[1].length;
|
||||
const lngLength = lng.split(".")[1].length;
|
||||
const result = checkAndParseLatLng(value);
|
||||
if (!result) return;
|
||||
const [lat, lng] = result;
|
||||
|
||||
const latControl = this.placeForm.get("lat");
|
||||
const lngControl = this.placeForm.get("lng");
|
||||
|
||||
latControl?.setValue(
|
||||
parseFloat(lat).toFixed(latLength > 5 ? 5 : latLength),
|
||||
{
|
||||
emitEvent: false,
|
||||
},
|
||||
);
|
||||
lngControl?.setValue(
|
||||
parseFloat(lng).toFixed(lngLength > 5 ? 5 : lngLength),
|
||||
{
|
||||
emitEvent: false,
|
||||
},
|
||||
);
|
||||
latControl?.setValue(formatLatLng(lat), { emitEvent: false });
|
||||
lngControl?.setValue(formatLatLng(lng), { emitEvent: false });
|
||||
|
||||
lngControl?.markAsDirty();
|
||||
lngControl?.updateValueAndValidity();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
<div class="grid md:grid-cols-5 gap-4">
|
||||
<p-floatlabel variant="in">
|
||||
<p-select [options]="days" optionValue="id" optionLabel="label" inputId="day_id" id="day_id"
|
||||
formControlName="day_id" [checkmark]="true" class="capitalize" fluid />
|
||||
formControlName="day_id" [checkmark]="true" class="capitalize" fluid>
|
||||
<ng-template let-day #item>
|
||||
<div class="whitespace-normal">{{ day.label }}</div>
|
||||
</ng-template>
|
||||
</p-select>
|
||||
<label for="day_id">Day</label>
|
||||
</p-floatlabel>
|
||||
|
||||
@ -26,7 +30,7 @@
|
||||
formControlName="place" [showClear]="true" class="capitalize" fluid>
|
||||
|
||||
<ng-template let-place #item>
|
||||
<div class="flex items-center gap-2" [class.text-gray-500]="place.placeUsage">
|
||||
<div class="flex items-center whitespace-normal gap-2" [class.text-gray-500]="place.placeUsage">
|
||||
<img [src]="place.image" class="rounded-full size-6" />
|
||||
<div>{{ place.name }}</div>
|
||||
</div>
|
||||
@ -61,6 +65,9 @@
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template let-option #item>
|
||||
<div class="whitespace-normal">{{ option.label }}</div>
|
||||
</ng-template>
|
||||
</p-select>
|
||||
<label for="status">Status</label>
|
||||
</p-floatlabel>
|
||||
|
||||
@ -15,6 +15,7 @@ import { SelectModule } from "primeng/select";
|
||||
import { TextareaModule } from "primeng/textarea";
|
||||
import { InputMaskModule } from "primeng/inputmask";
|
||||
import { UtilsService } from "../../services/utils.service";
|
||||
import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser";
|
||||
|
||||
@Component({
|
||||
selector: "app-trip-create-day-item-modal",
|
||||
@ -70,6 +71,7 @@ export class TripCreateDayItemModalComponent {
|
||||
"",
|
||||
{
|
||||
validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
|
||||
updateOn: "blur",
|
||||
},
|
||||
],
|
||||
lng: [
|
||||
@ -104,17 +106,26 @@ export class TripCreateDayItemModalComponent {
|
||||
this.itemForm.get("lat")?.setValue(p.lat);
|
||||
this.itemForm.get("lng")?.setValue(p.lng);
|
||||
this.itemForm.get("price")?.setValue(p.price || 0);
|
||||
if (!this.itemForm.get("text")?.value)
|
||||
this.itemForm.get("text")?.setValue(p.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.itemForm.get("lat")?.valueChanges.subscribe({
|
||||
next: (value: string) => {
|
||||
if (/\-?\d+\.\d+,\s\-?\d+\.\d+/.test(value)) {
|
||||
let [lat, lng] = value.split(", ");
|
||||
this.itemForm.get("lat")?.setValue(parseFloat(lat).toFixed(5));
|
||||
this.itemForm.get("lng")?.setValue(parseFloat(lng).toFixed(5));
|
||||
}
|
||||
const result = checkAndParseLatLng(value);
|
||||
if (!result) return;
|
||||
|
||||
const [lat, lng] = result;
|
||||
const latControl = this.itemForm.get("lat");
|
||||
const lngControl = this.itemForm.get("lng");
|
||||
|
||||
latControl?.setValue(formatLatLng(lat), { emitEvent: false });
|
||||
lngControl?.setValue(formatLatLng(lng), { emitEvent: false });
|
||||
|
||||
lngControl?.markAsDirty();
|
||||
lngControl?.updateValueAndValidity();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
<section [formGroup]="itemBatchForm" class="grid gap-4">
|
||||
<p-floatlabel variant="in">
|
||||
<p-select [options]="days" optionValue="id" optionLabel="label" inputId="day_id" id="day_id"
|
||||
formControlName="day_id" [checkmark]="true" class="w-full capitalize" fluid />
|
||||
formControlName="day_id" [checkmark]="true" class="w-full capitalize" fluid>
|
||||
<ng-template let-day #item>
|
||||
<div class="whitespace-normal">{{ day.label }}</div>
|
||||
</ng-template>
|
||||
</p-select>
|
||||
<label for="day_id">Day</label>
|
||||
</p-floatlabel>
|
||||
|
||||
|
||||
@ -10,7 +10,13 @@ import { SkeletonModule } from "primeng/skeleton";
|
||||
|
||||
@Component({
|
||||
selector: "app-trip-place-select-modal",
|
||||
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule],
|
||||
imports: [
|
||||
FloatLabelModule,
|
||||
InputTextModule,
|
||||
ButtonModule,
|
||||
ReactiveFormsModule,
|
||||
SkeletonModule,
|
||||
],
|
||||
standalone: true,
|
||||
templateUrl: "./trip-place-select-modal.component.html",
|
||||
styleUrl: "./trip-place-select-modal.component.scss",
|
||||
@ -27,11 +33,11 @@ export class TripPlaceSelectModalComponent {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private ref: DynamicDialogRef,
|
||||
private config: DynamicDialogConfig
|
||||
private config: DynamicDialogConfig,
|
||||
) {
|
||||
this.apiService.getPlaces().subscribe({
|
||||
next: (places) => {
|
||||
this.places = places;
|
||||
this.places = places.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.displayedPlaces = places;
|
||||
},
|
||||
});
|
||||
@ -51,7 +57,9 @@ export class TripPlaceSelectModalComponent {
|
||||
|
||||
let v = value.toLowerCase();
|
||||
this.displayedPlaces = this.places.filter(
|
||||
(p) => p.name.toLowerCase().includes(v) || p.description?.toLowerCase().includes(v)
|
||||
(p) =>
|
||||
p.name.toLowerCase().includes(v) ||
|
||||
p.description?.toLowerCase().includes(v),
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -62,7 +70,7 @@ export class TripPlaceSelectModalComponent {
|
||||
this.selectedPlacesID.splice(this.selectedPlacesID.indexOf(p.id), 1);
|
||||
this.selectedPlaces.splice(
|
||||
this.selectedPlaces.findIndex((place) => place.id === p.id),
|
||||
1
|
||||
1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
64
src/src/app/shared/latlng-parser.ts
Normal file
64
src/src/app/shared/latlng-parser.ts
Normal file
@ -0,0 +1,64 @@
|
||||
const patternDEC = /^\s*(-?\d{1,3}(?:\.\d+)?)\s*,\s*(-?\d{1,3}(?:\.\d+)?)\s*$/;
|
||||
const patternDD =
|
||||
/^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i;
|
||||
const patternDMS =
|
||||
/^\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([EW])\s*$/i;
|
||||
const patternDMM =
|
||||
/^\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([EW])\s*$/i;
|
||||
|
||||
function _dmsToDecimal(
|
||||
deg: number,
|
||||
min: number,
|
||||
sec: number,
|
||||
dir: string,
|
||||
): number {
|
||||
let dec = deg + min / 60 + sec / 3600;
|
||||
return /[SW]/i.test(dir) ? -dec : dec;
|
||||
}
|
||||
|
||||
function _dmmToDecimal(deg: number, min: number, dir: string): number {
|
||||
let dec = deg + min / 60;
|
||||
return /[SW]/i.test(dir) ? -dec : dec;
|
||||
}
|
||||
|
||||
export function formatLatLng(num: number): string {
|
||||
const decimals = num.toString().split(".")[1]?.length || 0;
|
||||
return num.toFixed(Math.min(decimals, 5));
|
||||
}
|
||||
|
||||
export function checkAndParseLatLng(str: string): [number, number] | undefined {
|
||||
// Parse DMS, DD, DDM to decimal [Lat, Lng]
|
||||
const dec = str.match(patternDEC);
|
||||
if (dec) {
|
||||
const lat = parseFloat(dec[1]);
|
||||
const lng = parseFloat(dec[2]);
|
||||
if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) {
|
||||
return [lat, lng];
|
||||
}
|
||||
}
|
||||
|
||||
const dd = str.match(patternDD);
|
||||
if (dd) {
|
||||
let lat = parseFloat(dd[1]);
|
||||
let lng = parseFloat(dd[3]);
|
||||
lat *= /S/i.test(dd[2]) ? -1 : 1;
|
||||
lng *= /W/i.test(dd[4]) ? -1 : 1;
|
||||
return [lat, lng];
|
||||
}
|
||||
|
||||
const dms = str.match(patternDMS);
|
||||
if (dms) {
|
||||
const lat = _dmsToDecimal(+dms[1], +dms[2], +dms[3], dms[4]);
|
||||
const lng = _dmsToDecimal(+dms[5], +dms[6], +dms[7], dms[8]);
|
||||
return [lat, lng];
|
||||
}
|
||||
|
||||
const dmm = str.match(patternDMM);
|
||||
if (dmm) {
|
||||
const lat = _dmmToDecimal(+dmm[1], +dmm[2], dmm[3]);
|
||||
const lng = _dmmToDecimal(+dmm[4], +dmm[5], dmm[6]);
|
||||
return [lat, lng];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -46,17 +46,6 @@ html {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
@keyframes slideY {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
.slideY {
|
||||
animation: slideY 0.3s both;
|
||||
}
|
||||
|
||||
.class-tooltip {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
@ -112,35 +101,22 @@ html {
|
||||
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.leaflet-contextmenu a.leaflet-contextmenu-item-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.leaflet-contextmenu a.leaflet-contextmenu-item.over {
|
||||
background-color: #f1f3f7;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.leaflet-contextmenu a.leaflet-contextmenu-item-disabled.over {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.leaflet-contextmenu-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.leaflet-contextmenu-separator {
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.image-marker {
|
||||
border-radius: 50%;
|
||||
border: 2px solid #405cf5;
|
||||
@ -167,3 +143,14 @@ html {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-x {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-x {
|
||||
animation: slide-x 0.3s both;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user