Skip to content

Source Toc

Source Toc

Cette page est générée automatiquement à partir du dépôt local au moment de la génération de la documentation.

Fichiers inclus

packages/common/src/lib/widgets/toc/toc.declaration.ts

packages/common/src/lib/widgets/toc/toc.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';
import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
import { TocState } from './toc.state.svelte';
import { type TocFullConfig, tocFullConfigSchema } from './toc.config';
export const declaration = {
factory: () => import('./Toc.svelte').then((Toc) => widgetFactorySvelte(Toc)),
schema: () => tocFullConfigSchema,
state: (props) => new TocState(props),
} satisfies WidgetDeclaration;
export type TocProps = WidgetProps<TocFullConfig, TocState>;

packages/common/src/lib/widgets/toc/toc.config.ts

packages/common/src/lib/widgets/toc/toc.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';
import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';
import { z } from 'zod';
import { tocI18n } from '$lib/widgets/toc/toc.i18n';
export const timeTravelControllerConfig = z.object({
showMetadataUrlButton: z.boolean().default(true),
autoPlayTransitionIsSeconds: z.number().default(2),
});
export type TimeTravelControllerConfig = z.infer<typeof timeTravelControllerConfig>;
export const tocConfigSchema = z.object({
dataTableWidgetId: z.string().optional(),
addDataWidgetId: z.string().default('addDataWidgetId'),
drawWidgetId: z.string().default('draw'),
showAddDataButton: z.boolean().default(true),
showClearAllButton: z.boolean().default(true),
showBasemapSelector: z.boolean().default(true),
showLayersLoadingState: z.boolean().default(true),
timeTravelControllerConfig: timeTravelControllerConfig.prefault({}),
maxLengthForServiceDescription: z.number().default(150),
});
export const tocFullConfigSchema = defineWidgetConfig({
title: tocI18n.title,
icon: {
lucide: 'Layers',
},
inToolbar: inToolbarSchemaFrom({
type: 'button',
}),
i18n: i18nSchemaFrom(tocI18n),
config: tocConfigSchema.prefault({}),
});
export type TocFullConfig = z.infer<typeof tocFullConfigSchema>;
export type TocConfig = z.infer<typeof tocConfigSchema>;
export type TocI18n = TocFullConfig['i18n'];

packages/common/src/lib/widgets/toc/components/ExportButton.svelte

packages/common/src/lib/widgets/toc/components/ExportButton.svelte
<script lang="ts">
import type { ApiGraphicsMapService } from '$lib/api/mapservices';
import ExportDialog from '$lib/components/common/ExportDialog.svelte';
import TocActionIcon from './TocActionIcon.svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
active?: boolean;
service: ApiGraphicsMapService;
parentVisible: boolean;
}
let { active = false, service, parentVisible, ...props }: Props = $props();
</script>
<ExportDialog exportFileName={service.label} features={service.features}>
<TocActionIcon
{...props}
{active}
icon={{ lucide: 'ExternalLink' }}
visibleOnMap={service.visible && parentVisible}
/>
</ExportDialog>

packages/common/src/lib/widgets/toc/components/ScaleMessage.svelte

packages/common/src/lib/widgets/toc/components/ScaleMessage.svelte
<script lang="ts">
import type { ApiMapService } from '$lib/api/mapservices';
import type { ApiSublayer } from '$lib/api/layers';
import { isMapService, isVisibleAtScale } from '$lib/api/utils/service.utils';
import { getMapManager } from '$lib/api/map';
import { onDestroy } from 'svelte';
import { getI18n } from '$lib/api/managers/i18n';
import type { TocI18n } from '$lib/widgets/toc/toc.config';
import { cn } from '$lib/components/shadcn/utils';
interface Props {
service: ApiMapService | ApiSublayer;
visibleAtCurrentScale: boolean;
i18n: TocI18n;
class?: string;
}
let { service, i18n: tocI18n, visibleAtCurrentScale = $bindable(false), class: className }: Props = $props();
const mapManager = getMapManager();
const scale = mapManager.tools.scale;
const i18n = getI18n(tocI18n);
let currentScale = $state<number | undefined>();
const message = $derived.by(() => {
if (!currentScale) return '';
const minScale = service.minScale;
const maxScale = service.maxScale;
if (minScale && maxScale && (minScale < currentScale || maxScale > currentScale)) {
return i18n('zoom-in-or-out', { minScale, maxScale });
}
if (minScale && minScale < currentScale) {
return i18n('visible-under', { minScale });
} else if (maxScale && maxScale > currentScale) {
return i18n('visible-above', { maxScale });
}
return '';
});
const scaleWatchUnsubscriber = mapManager.tools.events.watch(
'SCALE',
(newScale) => {
currentScale = newScale;
visibleAtCurrentScale = isVisibleAtScale(service, newScale);
},
{ initial: true },
);
$effect(() => {
if (isMapService(service)) {
service.onLoad(() => {
const initScale = scale.getCurrentScaleInfo()?.scaleForDevice;
if (initScale) {
visibleAtCurrentScale = isVisibleAtScale(service, initScale);
}
});
}
});
onDestroy(() => {
scaleWatchUnsubscriber();
});
</script>
{#if !visibleAtCurrentScale}
<span class={cn('gv-text-sm gv-text-heading', className)}>{message}</span>
{/if}

packages/common/src/lib/widgets/toc/components/TimeTravelController.svelte

packages/common/src/lib/widgets/toc/components/TimeTravelController.svelte
<script lang="ts">
import { type ApiTimeTravellable } from '$lib/api/mapservices';
import { cn } from '$lib/components/shadcn/utils';
import type { ApiMapServiceWithImageUrl } from '$lib/api/managers/configuration';
import { ChevronLeft } from 'lucide-svelte';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { buttonVariants } from '$lib/components/shadcn/ui/button';
import { getI18n } from '$lib/api/managers/i18n';
import type { TimeTravelControllerConfig, TocI18n } from '$lib/widgets/toc/toc.config';
interface Props {
service: ApiTimeTravellable;
config: TimeTravelControllerConfig;
i18n: TocI18n;
thumbnailIndex: number;
class?: string;
}
type MapserviceWithDynamicThumbnails = ApiMapServiceWithImageUrl & { thumbnailUrl?: string };
let { service, config, i18n: i18nConfig, class: className, thumbnailIndex = $bindable() }: Props = $props();
const i18n = getI18n(i18nConfig);
const numberOfVisibleElementAtATime = service.numberOfVisibleTiles;
const currentIndex = $derived.by(() => (service.selected ? service.servicesConfig.indexOf(service.selected) : 0));
const totalElements = $derived.by(() => service.servicesConfig.length);
const mapServicesConfig: MapserviceWithDynamicThumbnails[] = $derived.by(() => {
return service.servicesConfig.map((cfg) => ({
...cfg,
thumbnailUrl: cfg.imageUrl ?? `${cfg.url}/info/thumbnail`,
}));
});
const visiblePages = $derived.by(() => {
const maxPageToShow = numberOfVisibleElementAtATime;
if (mapServicesConfig.length <= maxPageToShow) {
return Array.from({ length: mapServicesConfig.length }, (_, i) => i);
}
return Array.from({ length: maxPageToShow }, (_, i) => thumbnailIndex + i);
});
$effect(() => {
const half = Math.trunc(numberOfVisibleElementAtATime / 2);
if (
currentIndex >= half &&
currentIndex < totalElements - (numberOfVisibleElementAtATime - 1 - half) &&
currentIndex > 0
) {
thumbnailIndex = currentIndex - half;
}
});
function selectMapService(mapService: ApiMapServiceWithImageUrl): void {
service.select(mapService);
}
function previousImage() {
if (thumbnailIndex > 0) {
thumbnailIndex -= 1;
}
}
function nextImage() {
if (thumbnailIndex < totalElements - numberOfVisibleElementAtATime) {
thumbnailIndex += 1;
}
}
function isSelected(mapServiceConfig: ApiMapServiceWithImageUrl) {
return mapServiceConfig.id === service.selected?.id;
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowRight') {
nextImage();
event.stopPropagation();
} else if (event.key === 'ArrowLeft') {
previousImage();
event.stopPropagation();
}
}
</script>
<div role="button" tabindex="0" onkeydown={(evt) => handleKeyDown(evt)} class={cn('gv-space-y-2.5', className)}>
<div class="gv-relative gv-w-full gv-flex gv-items-center gv-gap-1">
{#if thumbnailIndex > 0}
<div
class="gv-bg-gradient-to-l gv-from-background/0 gv-to-background gv-absolute gv-left-0 gv-w-20 gv-h-full gv-z-10 gv-pointer-events-none"
>
<button
onclick={previousImage}
class={cn(
'gv-absolute gv-left-0 gv-top-1/2 gv-transform gv--translate-y-1/2 gv-z-15 gv-pointer-events-auto',
)}
data-test-id="TimeTravel-PreviousPage"
>
<ChevronLeft />
</button>
</div>
{/if}
<div class="gv-flex gv-flex-1 gv-overflow-hidden gv-gap-0.5 gv-relative">
<!-- Handle service load error (in red and not clickable ?)-->
{#each visiblePages as pageIndex (pageIndex)}
{@const config = mapServicesConfig[pageIndex]}
<button
title={config.label}
onclick={() => selectMapService(config)}
class={cn(
'gv-flex-1 gv-text-center gv-relative',
isSelected(config) ? 'gv-border-solid gv-border-2 gv-border-primary' : 'gv-opacity-50 gv-mt-2',
)}
data-test-id={`TimeTravel-${config.id}`}
data-test-element-index={pageIndex}
>
<img
src={config.thumbnailUrl}
alt="Thumbnail of map service"
class="gv-w-full gv-h-16 gv-object-cover"
/>
<span class={cn(isSelected(config) && 'gv-text-primary')}>
{config.shortLabel}
</span>
</button>
{/each}
</div>
{#if thumbnailIndex < totalElements - numberOfVisibleElementAtATime}
<div
class="gv-bg-gradient-to-r gv-from-background/0 gv-to-background gv-absolute gv-right-0 gv-w-20 gv-h-full gv-z-10 gv-pointer-events-none"
>
<button
onclick={nextImage}
class={cn(
'gv-absolute gv-right-0 gv-top-1/2 gv-transform gv--translate-y-1/2 gv-z-15 gv-pointer-events-auto',
)}
data-test-id="TimeTravel-NextPage"
>
<ChevronRight />
</button>
</div>
{/if}
</div>
{#if config.showMetadataUrlButton && service.selected?.metadataUrl}
<div class="gv-flex gv-justify-start">
<a
href={service.selected.metadataUrl}
target="_blank"
rel="noopener noreferrer"
class={buttonVariants({ variant: 'outline' })}
data-test-id="TimeTravel-MetadataLink"
>
{i18n('view-complete-label', { label: service.selected.label })}
</a>
</div>
{/if}
</div>

packages/common/src/lib/widgets/toc/components/TimeTravelIcon.svelte

packages/common/src/lib/widgets/toc/components/TimeTravelIcon.svelte
<script lang="ts">
import type { ApiTimeTravellable } from '$lib/api/mapservices';
import type { TimeTravelControllerConfig } from '$lib/widgets/time-travel-controller/time-travel-controller.config';
import { onDestroy } from 'svelte';
import TocActionIcon from './TocActionIcon.svelte';
interface Props {
service: ApiTimeTravellable;
config: TimeTravelControllerConfig;
onPlayClicked: () => void;
}
let { service, config, onPlayClicked = () => {} }: Props = $props();
let isPlaying = $state<boolean>(false);
let interval: NodeJS.Timeout | undefined;
function play() {
onPlayClicked();
service.visible = true;
if (!service.selected) return;
isPlaying = true;
const lastIndex = service.servicesConfig.length - 1;
let currentIndex = service.servicesConfig.indexOf(service.selected);
// Play from start if last service is currently selected
if (lastIndex === currentIndex) {
service.select(service.servicesConfig[0]);
currentIndex = service.servicesConfig.indexOf(service.selected);
}
interval = setInterval(() => {
if (currentIndex === lastIndex) {
pause();
return;
}
currentIndex += 1;
service.select(service.servicesConfig[currentIndex]);
}, config.autoPlayTransitionIsSeconds * 1000);
}
function pause() {
isPlaying = false;
if (interval) {
clearInterval(interval);
}
}
onDestroy(() => {
if (interval) {
clearInterval(interval);
}
});
</script>
{#if isPlaying}
<TocActionIcon onclick={() => pause()} icon={{ lucide: 'Pause' }} visibleOnMap={service.visible} />
{:else}
<TocActionIcon onclick={() => play()} icon={{ lucide: 'Play' }} visibleOnMap={service.visible} />
{/if}

packages/common/src/lib/widgets/toc/components/TocActionIcon.svelte

packages/common/src/lib/widgets/toc/components/TocActionIcon.svelte
<script lang="ts">
import type { Icon as IconSchemaType } from '$lib/api/icons';
import { Icon } from '$lib/components/icon';
import { cn } from '$lib/components/shadcn/utils';
import type { HTMLButtonAttributes } from 'svelte/elements';
import { ApiSimpleTooltip } from '$lib/components/api-simple-tooltip';
interface Props extends HTMLButtonAttributes {
active?: boolean;
visibleOnMap?: boolean;
icon: IconSchemaType;
iconClass?: string;
tooltip?: string;
}
const {
active,
icon,
visibleOnMap = true,
class: className,
iconClass,
onclick,
disabled,
tooltip,
...restProps
}: Props = $props();
const onButtonClick: HTMLButtonAttributes['onclick'] = (event) => {
event.stopPropagation();
onclick?.(event);
};
</script>
<ApiSimpleTooltip text={tooltip}>
<button
{...restProps}
onclick={onButtonClick}
class={cn(
'gv-px-1.5 last:gv-pr-0 gv-border-r last:gv-border-none',
visibleOnMap && active && 'gv-text-primary-300',
visibleOnMap && !active && 'gv-text-primary-500 hover:gv-text-primary-300',
!visibleOnMap && active && 'gv-text-heading hover:gv-text-black',
!visibleOnMap && !active && 'gv-text-body hover:gv-text-black',
disabled && 'gv-text-body gv-pointer-events-none',
className,
)}
>
<Icon {icon} class={cn('gv-size-4', iconClass)} />
</button>
</ApiSimpleTooltip>

packages/common/src/lib/widgets/toc/components/TocBasemapSelector.svelte

packages/common/src/lib/widgets/toc/components/TocBasemapSelector.svelte
<script lang="ts">
import { Icon } from '$lib/components/icon';
import { getMapManager } from '$lib/api/map';
import { Button } from '$lib/components/shadcn/ui/button';
import { getI18n } from '$lib/api/managers/i18n';
import type { TocI18n } from '$lib/widgets/toc/toc.config';
import { SquareArrowOutUpRight } from 'lucide-svelte';
import ChevronUp from 'lucide-svelte/icons/chevron-up';
import ChevronDown from 'lucide-svelte/icons/chevron-down';
import { cn } from '$lib/components/shadcn/utils';
import BasemapDetails from '$lib/widgets/basemap-chooser/BasemapDetails.svelte';
import { fly } from 'svelte/transition';
import type { ApiBasemap } from '$lib/api/mapservices';
import { calculateThumbnailUrl } from '$lib/api/utils';
import { getTopicManager } from '$lib/api/managers/topic';
interface Props {
i18nRegistry: TocI18n;
open: boolean;
}
let { i18nRegistry, open = $bindable() }: Props = $props();
const i18n = getI18n(i18nRegistry);
const mapManager = getMapManager();
const basemapList = mapManager.basemapList;
const topic = getTopicManager();
const selectedBasemapThumbnail = $derived.by(() =>
basemapList.selected ? calculateThumbnailUrl(basemapList.selected) : undefined,
);
let makeItAnimate = $state(true);
// 100% worth it
$effect(() => {
if (open) {
makeItAnimate = false;
setTimeout(() => {
makeItAnimate = true;
});
}
});
function onBasemapClick(basemap: ApiBasemap) {
basemapList.select(basemap);
topic.publish({
type: 'BasemapChooser-select',
basemap,
source: 'Toc',
});
}
</script>
{#if basemapList.selected && makeItAnimate}
<div in:fly={{ duration: 600, y: '100dvh' }} class="gv-border-t">
<button
class="gv-w-full gv-h-20 gv-p-2 gv-flex hover:gv-bg-grey-50"
onclick={() => (open = !open)}
data-test-id="Toc-Basemap-Expand-Toggle"
>
<div class="gv-h-full gv-w-fit gv-aspect-square">
{#if basemapList.selected.icon}
<Icon icon={basemapList.selected.icon} alt="basemap-image" />
{:else if selectedBasemapThumbnail}
<img src={selectedBasemapThumbnail} alt="basemap thumbnail" />
{:else}
<div class="gv-h-full gv-bg-muted"></div>
{/if}
</div>
<div class="gv-h-full gv-w-full gv-flex-1 gv-flex gv-flex-col gv-justify-between gv-text-left gv-ml-2">
<span class="gv-font-bold">{i18n('basemap-title')}</span>
<span class="gv-text-md gv-text-primary-500">{basemapList.selected.label}</span>
<Button
variant="outline"
href={basemapList.selected.metadataUrl}
target="_blank"
onclick={(event: MouseEvent) => event.stopPropagation()}
size="sm"
class={cn('gv-w-fit', basemapList.selected.metadataUrl ? 'gv-visible' : 'gv-invisible')}
data-test-id="Toc-Basemap-Link-Metadata"
>
{i18n('basemap-view-complete-data')}
<SquareArrowOutUpRight class="gv-p-1" />
</Button>
</div>
{#if open}
<ChevronDown />
{:else}
<ChevronUp />
{/if}
</button>
{#if open}
<div>
{#each basemapList.list as basemap (basemap.id)}
{@const thumbnailUrl = calculateThumbnailUrl(basemap)}
<button
data-state={basemap.selected ? 'active' : ''}
class="gv-h-14 gv-w-full gv-p-2 gv-flex gv-items-center gv-border-t data-active:gv-font-bold hover:gv-bg-grey-50"
onclick={() => onBasemapClick(basemap)}
data-test-id={`Toc-Basemap-Select-${basemap.id}`}
>
<div
class={cn(
'gv-h-full gv-w-fit gv-aspect-square gv-border-2 gv-p-[2px] gv-border-transparent',
basemap.selected ? 'gv-border-primary' : '',
)}
>
{#if basemap.icon}
<Icon icon={basemap.icon} alt="basemap-image" />
{:else if thumbnailUrl}
<img class="gv-size-8" src={thumbnailUrl} alt="basemap thumbnail" />
{:else}
<div class="gv-h-full gv-bg-muted"></div>
{/if}
</div>
<span
class={cn(
'gv-flex-1 gv-text-md gv-text-left gv-ml-3',
!basemap.selected ? 'gv-text-primary' : '',
)}
>
{basemap.label}
</span>
</button>
{/each}
<div class="gv-p-2">
<BasemapDetails
basemap={basemapList.selected}
{i18nRegistry}
defaultOpen={true}
showMetaDataUrl={false}
/>
</div>
</div>
{/if}
</div>
{/if}

packages/common/src/lib/widgets/toc/components/TocItem.svelte

packages/common/src/lib/widgets/toc/components/TocItem.svelte
<script lang="ts">
import type { ApiSublayer } from '$lib/api/layers/api-sublayer.svelte';
import { getI18n } from '$lib/api/managers/i18n';
import { type ApiMapService } from '$lib/api/mapservices';
import { type BaseListWrapper, truncate } from '$lib/api/utils';
import { LegendCacheItem } from '$lib/api/utils/legend/legend.model.svelte';
import { isGraphicService, isMapService, isTimeTravelService } from '$lib/api/utils/service.utils';
import Error from '$lib/components/common/Error.svelte';
import OpacitySlider from '$lib/components/opacity-slider/OpacitySlider.svelte';
import { Label } from '$lib/components/shadcn/ui/label';
import { cn } from '$lib/components/shadcn/utils';
import LegendItemDisplay from '$lib/widgets/legend/legend-list/LegendItemDisplay.svelte';
import { isMapServiceWithUrl } from '$lib/widgets/legend/models/legend-data.svelte';
import TocItem from './TocItem.svelte';
import Loader from 'lucide-svelte/icons/loader';
import ScaleMessage from './ScaleMessage.svelte';
import TimeTravelController from './TimeTravelController.svelte';
import { Sortable } from '$lib/components/sortable';
import TocLayerIcon from './TocLayerIcon.svelte';
import TocLayerActions from './TocLayerActions.svelte';
import { getTopicManager } from '$lib/api/managers/topic';
import type { TocConfig, TocI18n } from '../toc.config';
import { getState, type TocItemState } from '../toc.state.svelte';
import { ExternalLink } from 'lucide-svelte';
import { hasSublayers, isSublayer } from '$lib/api/layers';
import { Button } from '$lib/components/shadcn/ui/button';
import ServiceLoadingStatus from '$lib/components/service-loading-status/ServiceLoadingStatus.svelte';
interface Props {
isRoot?: boolean;
state: TocItemState;
layerList: BaseListWrapper<ApiMapService | ApiSublayer>;
i18nConfig: TocI18n;
placeholder: string;
parentVisible?: boolean;
open?: boolean;
highlight?: boolean;
legendCacheItem?: LegendCacheItem;
class?: string;
headerClass?: string;
dataTestIdPrefix?: string;
config: TocConfig;
headerElement?: HTMLDivElement;
}
let {
isRoot = false,
state: itemState,
layerList,
i18nConfig,
placeholder,
highlight,
class: className,
headerClass,
open = $bindable(false),
parentVisible = true,
legendCacheItem = undefined,
dataTestIdPrefix,
config,
headerElement: thisHeaderElement = $bindable(),
}: Props = $props();
const topic = getTopicManager();
const i18n = getI18n(i18nConfig);
const tocState = getState();
let visibleAtCurrentScale = $state(true);
let thumbnailIndex = $state<number>(0);
const service = $derived(itemState.service);
const dataTestId = $derived(`${dataTestIdPrefix ?? ''}${i18n.translate(service.label)}`);
const canExpand = $derived(
(isMapService(service) && hasSublayers(service)) || isSublayer(service) || isTimeTravelService(service),
);
if (!legendCacheItem && isMapServiceWithUrl(service)) {
legendCacheItem = new LegendCacheItem(service);
}
function onHeaderClick() {
if (canExpand) {
open = !open;
}
}
function onHeaderKeyDown(event: KeyboardEvent) {
if (event.key === ' ') {
onHeaderClick();
event.stopPropagation();
}
}
function onMetadataUrlClick() {
topic.publish({
type: 'Toc-Layer-metadata-open',
layer: service as ApiMapService,
});
}
</script>
<div
class={cn(
'gv-flex gv-flex-col gv-w-full gv-transition-colors',
!service.visible && 'gv-bg-muted-500',
highlight && 'gv-animate-blink',
className,
)}
>
<!--Layer expand toggle-->
<!--TODO: Rename data-test-id-->
<div
bind:this={thisHeaderElement}
class={cn(
'gv-group gv-px-5 gv-text-start gv-py-3.5 gv-transition-colors gv-border-2 gv-border-transparent',
parentVisible && 'hover:gv-bg-primary-100',
(!service.visible || !parentVisible) && 'hover:gv-bg-muted-600',
!canExpand && 'gv-cursor-grab',
isGraphicService(service) && service.editing && 'gv-border-2 gv-border-primary',
headerClass,
)}
role="button"
tabindex="0"
onclick={onHeaderClick}
onkeydown={onHeaderKeyDown}
data-test-id={`${dataTestId}-Toc-Chevron`}
>
<!--Info row (icon - label - actions)-->
<div class="gv-flex gv-gap-2 gv-items-center">
<!--Layer icon-->
<TocLayerIcon
{isRoot}
{service}
{open}
{parentVisible}
class={!visibleAtCurrentScale ? 'gv-opacity-50' : ''}
/>
<div class="gv-flex-1">
<!--Layer name-->
<Label
data-test-id={`${dataTestId}-Toc-label`}
class={cn(
'gv-cursor-inherit gv-font-bold gv-duration-300 gv-text-wrap gv-whitespace-nowrap',
isMapService(service) && 'gv-cursor-inherit',
!visibleAtCurrentScale && 'gv-opacity-50',
)}
>
{i18n.translate(service.label)}
</Label>
<!--Graphic layer editing message-->
{#if isGraphicService(service) && service.editing}
<span class="gv-text-sm gv-text-primary-500 gv-font-bold gv-ml-2">{i18n('editing')}</span>
{/if}
</div>
{#if config.showLayersLoadingState}
<ServiceLoadingStatus {service} />
{/if}
<!--Layer actions-->
<TocLayerActions
bind:showActions={itemState.showActions}
bind:showInfo={itemState.showInfo}
bind:showOpacity={itemState.showOpacity}
{service}
{dataTestId}
{layerList}
{parentVisible}
{config}
onPlayClicked={() => (open = true)}
i18n={i18nConfig}
/>
</div>
<ScaleMessage i18n={i18nConfig} {service} bind:visibleAtCurrentScale class="gv-pl-6" />
</div>
<!--Opacity-->
{#if itemState.showOpacity && isMapService(service)}
<OpacitySlider
class="gv-pl-6 gv-pr-7 gv-py-3"
inputClass={cn('gv-bg-transparent', !service.visible && 'gv-text-body gv-border-body')}
bind:opacity={service.opacity}
variant={service.visible ? 'default' : 'secondary'}
showOpacityNumber
/>
{/if}
<!--Description-->
{#if itemState.showInfo && isMapService(service)}
<div class="gv-text-sm gv-ml-11 gv-pt-2.5 gv-pb-3">
<article title={service.description}>
{truncate(service.description, config.maxLengthForServiceDescription)}
</article>
{#if service.metadataUrl}
<Button
href={service.metadataUrl}
variant="link"
size="sm"
target="_blank"
onclick={onMetadataUrlClick}
>
<ExternalLink class="gv-size-4" />
{i18n('view-complete')}
</Button>
{/if}
</div>
{/if}
<!--Children-->
{#if hasSublayers(service) && service.sublayers && open}
<Sortable
items={service.sublayers.list}
getId={(s) => s.id.toString()}
onSort={(ids) => service.sublayers.sort(ids)}
reversed
class={cn('gv-divide-y', isRoot ? 'gv-pl-10' : 'gv-pl-5')}
>
{#snippet children({ item: sublayer, bindDragTrigger })}
{@const state = tocState.get(sublayer)}
<TocItem
bind:headerElement={() => {}, bindDragTrigger}
bind:open={state.open}
{legendCacheItem}
{state}
{i18nConfig}
{config}
parentVisible={service.visible && parentVisible}
dataTestIdPrefix={`${dataTestId}-`}
layerList={service.sublayers}
{placeholder}
headerClass="gv-pl-0 gv-py-1.5"
/>
{/snippet}
</Sortable>
{/if}
<!--Legend-->
{#if open && legendCacheItem}
{#if legendCacheItem.loading}
<Loader />
{:else if legendCacheItem.error}
<Error message={legendCacheItem.error.message} />
{:else if legendCacheItem.data}
<LegendItemDisplay legendItems={legendCacheItem.getLegendForLayer(`${service.id}`)} class="gv-ml-10" />
{/if}
{/if}
<!--Time Travel-->
{#if open && isTimeTravelService(service)}
<TimeTravelController
config={config.timeTravelControllerConfig}
i18n={i18nConfig}
bind:thumbnailIndex
{service}
class="gv-px-5 gv-pb-3.5"
/>
{/if}
</div>
<style>
@keyframes gv-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.gv-animate-blink {
animation: gv-blink 0.8s infinite;
}
</style>

packages/common/src/lib/widgets/toc/components/TocLayerActions.svelte

packages/common/src/lib/widgets/toc/components/TocLayerActions.svelte
<script lang="ts">
import {
type BaseListWrapper,
isGraphicService,
isMapService,
isQueryable,
isTimeTravelService,
type Queryable,
} from '$lib/api/utils';
import ExportButton from './ExportButton.svelte';
import TocActionIcon from './TocActionIcon.svelte';
import ZoomToExtentButton from './ZoomToExtentButton.svelte';
import type { ApiMapService } from '$lib/api/mapservices';
import { type ApiSublayer, hasSublayers } from '$lib/api/layers';
import { getWidgetManager } from '$lib/api/managers/widget';
import { getI18n } from '$lib/api/managers/i18n';
import { getMapManager } from '$lib/api/map';
import type Draw from '$lib/widgets/draw/Draw.svelte';
import type { TocConfig, TocI18n } from '../toc.config';
import TimeTravelIcon from './TimeTravelIcon.svelte';
import Trash2 from 'lucide-svelte/icons/trash-2';
import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte';
import { Button } from '$lib/components/shadcn/ui/button';
import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model';
import { getTopicManager } from '$lib/api/managers/topic';
import type ServiceDataTableWidget from '$lib/widgets/service-data-table/ServiceDataTableWidget.svelte';
import { cn } from '$lib/components/shadcn/utils';
import { getTableId } from '$lib/widgets/service-data-table/service-data-table/service-data-table.model';
interface Props {
service: ApiMapService | ApiSublayer;
config: TocConfig;
dataTestId: string;
showActions: boolean;
showInfo: boolean;
showOpacity: boolean;
i18n: TocI18n;
onPlayClicked: () => void;
parentVisible: boolean;
layerList: BaseListWrapper<ApiMapService | ApiSublayer>;
class?: string;
}
let {
service,
config,
dataTestId,
showActions = $bindable(),
showInfo = $bindable(),
showOpacity = $bindable(),
i18n: tocI18n,
layerList,
parentVisible,
class: className,
onPlayClicked = () => {},
}: Props = $props();
const widgetManager = getWidgetManager();
const mapManager = getMapManager();
const topic = getTopicManager();
const i18n = getI18n(tocI18n);
let confirmDeleteOpen = $state(false);
const graphicMapService = $derived(isGraphicService(service) ? service : null);
const mapService = $derived(isMapService(service) ? service : null);
const timeTravel = $derived(isTimeTravelService(service) ? service : null);
const queryableService = $derived(
config.dataTableWidgetId &&
isQueryable(service) &&
!hasSublayers(service) &&
widgetManager.hasWidgetClass('ServiceDataTable')
? service
: null,
);
const showTableIcon = $derived(
config.dataTableWidgetId && !hasSublayers(service) && widgetManager.hasWidgetClass('ServiceDataTable')
? service
: null,
);
function launchDraw() {
if (!config.drawWidgetId) {
throw new GeoviewerError('No drawWidgetId provided');
}
if (!isGraphicService(service)) {
return;
}
widgetManager
.getReference<Draw>(config.drawWidgetId)
.activate()
.then((widget) => widget.initFromService(service));
}
async function getServiceDataTableWidget(): Promise<ServiceDataTableWidget> {
if (!config.dataTableWidgetId) {
throw new GeoviewerError('No dataTableWidgetId provided');
}
return await widgetManager.getReference<ServiceDataTableWidget>(config.dataTableWidgetId).activate();
}
async function openDataTable(queryable: (typeof service & Queryable) | null) {
if (!queryable) return;
const tabName = queryable.label ?? '';
const tabId = getTableId(queryable);
const dataTableWidget = await getServiceDataTableWidget();
dataTableWidget.openDataTable({ tabName, service: queryable, tabId });
}
function toggleDescription() {
showInfo = !showInfo;
showOpacity = false;
if (showInfo) {
topic.publish({
type: 'Toc-Layer-description-open',
layer: mapService!,
});
}
}
function onShowOpacity() {
showOpacity = !showOpacity;
showInfo = false;
}
function confirmDelete() {
confirmDeleteOpen = true;
}
function deleteLayer() {
deleteAssociatedServiceDataTableResults();
layerList.remove(service);
topic.publish({
type: 'Toc-Layer-remove',
layer: service,
});
}
async function deleteAssociatedServiceDataTableResults() {
const serviceDataTableWidget = await getServiceDataTableWidget();
serviceDataTableWidget.closeAllPanelsForServiceId(getTableId(service));
}
function toggleVisibility(toggle: boolean) {
service.visible = toggle;
topic.publish({
type: 'Toc-Layer-visibility-toggle',
layer: service,
toggle,
});
}
</script>
<div class={cn('gv-flex gv-flex-row gv-items-center', className)}>
{#if showTableIcon}
<TocActionIcon
data-test-id={`${dataTestId}-Toc-Search`}
title={i18n('common.search')}
disabled={!queryableService}
tooltip={queryableService ? '' : i18n('query-disabled')}
onclick={() => openDataTable(queryableService)}
icon={{ lucide: 'TableProperties' }}
visibleOnMap={service.visible && parentVisible}
class="gv-hidden group-hover:gv-inline-block"
/>
{/if}
{#if isTimeTravelService(service)}
<TimeTravelIcon {onPlayClicked} {service} config={config.timeTravelControllerConfig} />
{/if}
<!--Graphic service edition-->
{#if graphicMapService && !graphicMapService.editing && widgetManager.hasWidgetClass('Draw')}
<TocActionIcon
data-test-id={`${dataTestId}-Toc-Draw`}
title={i18n('common.edit')}
onclick={() => launchDraw()}
icon={{ lucide: 'Pencil' }}
visibleOnMap={service.visible && parentVisible}
/>
{/if}
<!--Layer visible-->
{#if service.visible}
<TocActionIcon
data-test-id={`${dataTestId}-Toc-hide`}
title={i18n('common.hide')}
onclick={() => toggleVisibility(false)}
icon={{ lucide: 'Eye' }}
visibleOnMap={parentVisible}
/>
{:else}
<TocActionIcon
data-test-id={`${dataTestId}-Toc-show`}
title={i18n('common.show')}
onclick={() => toggleVisibility(true)}
icon={{ lucide: 'EyeOff' }}
visibleOnMap={false}
/>
{/if}
{#if showActions}
{#if graphicMapService}
<ExportButton
data-test-id={`${dataTestId}-Toc-export`}
title={i18n('common.export')}
service={graphicMapService}
{parentVisible}
/>
{/if}
{#if mapService}
<ZoomToExtentButton
data-test-id={`${dataTestId}-Toc-extent`}
title={i18n('zoom-to-extent')}
service={mapService}
noExtentMessage={i18n('no-extent')}
{mapManager}
{parentVisible}
/>
{#if !graphicMapService && !timeTravel}
<TocActionIcon
title={i18n('common.description')}
data-test-id={`${dataTestId}-Toc-description`}
active={showInfo}
onclick={toggleDescription}
icon={{ lucide: 'Info' }}
visibleOnMap={service.visible && parentVisible}
/>
{/if}
<TocActionIcon
title={i18n('opacity')}
active={showOpacity}
data-test-id={`${dataTestId}-Toc-opacity`}
onclick={onShowOpacity}
icon={{ lucide: 'Contrast' }}
visibleOnMap={service.visible && parentVisible}
/>
{/if}
{#if mapService && mapService.removable}
<TocActionIcon
data-test-id={`${dataTestId}-Toc-remove`}
title={i18n('remove')}
onclick={() => confirmDelete()}
disabled={graphicMapService?.editing}
icon={{ lucide: 'Trash2' }}
visibleOnMap={service.visible && parentVisible}
/>
{/if}
{/if}
<!--Toggle actions-->
{#if graphicMapService || mapService}
<TocActionIcon
title={i18n('other-actions')}
data-test-id={`${dataTestId}-Toc-actions-menu`}
onclick={() => (showActions = !showActions)}
icon={{ lucide: 'Ellipsis' }}
visibleOnMap={service.visible && parentVisible}
/>
{/if}
</div>
<ConfirmDialog message={i18n('confirm-delete-message')} bind:open={confirmDeleteOpen}>
{#snippet footer()}
<Button
onclick={() => (confirmDeleteOpen = false)}
variant="outline"
data-test-id="TocItem-ConfirmDelete-Cancel"
>
{i18n('common.cancel')}
</Button>
<Button onclick={deleteLayer} data-test-id="TocItem-ConfirmDelete-Confirm" variant="destructive">
<Trash2 class="gv-size-4" />
{i18n('common.confirm')}
</Button>
{/snippet}
</ConfirmDialog>

packages/common/src/lib/widgets/toc/components/TocLayerIcon.svelte

packages/common/src/lib/widgets/toc/components/TocLayerIcon.svelte
<script lang="ts">
import { cn } from '$lib/components/shadcn/utils';
import { isMapService, isTimeTravelService } from '$lib/api/utils';
import History from 'lucide-svelte/icons/history';
import Map from 'lucide-svelte/icons/map';
import { Icon } from '$lib/components/icon';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
import type { ApiMapService } from '$lib/api/mapservices';
import type { ApiSublayer } from '$lib/api/layers';
import { Tangent } from 'lucide-svelte';
interface Props {
isRoot: boolean;
service: ApiMapService | ApiSublayer;
open: boolean;
parentVisible: boolean;
class?: string;
}
let { isRoot, service, open, parentVisible, class: className }: Props = $props();
const iconClass = $derived(
cn('gv-size-4 gv-text-body', service.visible && parentVisible && 'gv-text-primary-500', className),
);
const isFeatureService = $derived.by(
() => isMapService(service) && ['ARCGIS_FEATURE_SERVICE', 'ARCGIS_FEATURE_LAYER'].indexOf(service.type) > -1,
);
</script>
{#if isRoot}
<div class="gv-block group-hover:gv-hidden">
{#if isMapService(service)}
{#if service.type === 'GRAPHICS'}
<Icon class={iconClass} icon={{ geoviewer: 'graphic-layer' }} />
{:else if isFeatureService}
<Tangent class={iconClass} />
{:else if isTimeTravelService(service)}
<History class={iconClass} />
{:else}
<Map class={iconClass} />
{/if}
{:else}
<Map class={iconClass} />
{/if}
</div>
{:else}
<!--Expand chevron-->
<ChevronRight class={cn(iconClass, 'gv-duration-150 group-hover:gv-hidden', open && 'gv-rotate-90')} />
{/if}
<ChevronsUpDown class={cn(iconClass, 'gv-hidden group-hover:gv-inline-block')} />

packages/common/src/lib/widgets/toc/components/ZoomToExtentButton.svelte

packages/common/src/lib/widgets/toc/components/ZoomToExtentButton.svelte
<script lang="ts">
import type { MapManager } from '$lib/api/map';
import type { ApiMapService } from '$lib/api/mapservices';
import TocActionIcon from './TocActionIcon.svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
import { showToast } from '$lib/components/toast/toast.utils';
import { isTimeTravelService } from '$lib/api/utils';
interface Props extends HTMLButtonAttributes {
service: ApiMapService;
mapManager: MapManager;
parentVisible: boolean;
noExtentMessage: string;
}
let { service, mapManager, parentVisible, noExtentMessage, ...props }: Props = $props();
function zoomToFullExtent() {
let fullExtent = service.extent;
if (isTimeTravelService(service)) {
fullExtent = service.services[0].extent;
}
if (fullExtent) {
mapManager.tools.zoom.zoomToExtent(fullExtent);
} else {
showToast({ level: 'info', message: noExtentMessage });
}
}
</script>
<TocActionIcon
{...props}
onclick={zoomToFullExtent}
icon={{ lucide: 'Fullscreen' }}
visibleOnMap={service.visible && parentVisible}
/>

packages/common/src/lib/widgets/toc/toc.i18n.ts

packages/common/src/lib/widgets/toc/toc.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
import { basemapChooserI18n } from '$lib/widgets/basemap-chooser/basemap-chooser.i18n';
export const tocI18n = {
title: {
fr: 'Données',
nl: 'NL - Données',
},
'add-data': {
fr: 'Ajouter des données',
nl: 'NL - Ajouter des données',
},
'empty-selection': {
fr: 'Supprimer toutes les données',
nl: 'NL - Supprimer toutes les données',
},
'empty-selection-confirmation': {
fr: 'Voulez-vous supprimer le contenu actuel de votre carte ?',
nl: 'NL - Voulez-vous supprimer le contenu actuel de votre carte ?',
},
'layer-label-empty': {
fr: 'Couche sans titre',
nl: 'NL - Couche sans titre',
},
'zoom-to-extent': {
fr: "Zoomer sur l'emprise",
nl: "NL - Zoomer sur l'emprise",
},
'no-extent': {
fr: "L'emprise n'a pas pu être calculée",
nl: "NL - L'emprise n'a pas pu être calculée",
},
'other-actions': {
fr: "Autres d'actions",
nl: "NL - Autres d'actions",
},
'query-disabled': {
fr: 'Fonctionnalité non disponible pour cette donnée',
nl: 'NL - Fonctionnalité non disponible pour cette donnée',
},
'confirm-delete-message': {
fr: 'Voulez-vous supprimer la donnée de la carte ?',
nl: 'NL - Voulez-vous supprimer la donnée de la carte ?',
},
remove: {
fr: 'Retirer de la carte',
nl: 'NL - Retirer de la carte',
},
opacity: {
fr: "Modifier l'opacité",
nl: "NL - Modifier l'opacité",
},
editing: {
fr: 'Edition en cours',
nl: 'NL - Edition en cours',
},
'view-complete': {
fr: 'Ouvrir la fiche descriptive',
nl: 'NL - Ouvrir la fiche descriptive',
},
'view-complete-label': {
fr: 'Ouvrir la fiche descriptive de {{label}}',
nl: 'NL - Ouvrir la fiche descriptive de {{label}}',
},
'visible-above': {
fr: 'Zoomez jusque 1:{{maxScale}}',
nl: 'NL - Zoomez jusque 1:{{maxScale}}',
},
'visible-under': {
fr: 'Zoomez à partir de 1:{{minScale}}',
nl: 'NL - Zoomez à partir de 1:{{minScale}}',
},
'zoom-in-or-out': {
fr: 'Zoomez ou dézoomez pour voir les données (1:{{minScale}} - 1:{{maxScale}})',
nl: 'NL - Zoomez ou dézoomez pour voir les données (1:{{minScale}} - 1:{{maxScale}})',
},
'basemap-title': {
fr: 'Fond de plan',
nl: 'NL - Fond de plan',
},
'basemap-view-complete-data': {
fr: 'Ouvrir la fiche descriptive',
nl: 'NL - Ouvrir la fiche descriptive',
},
...basemapChooserI18n,
} satisfies I18nRegistry;

packages/common/src/lib/widgets/toc/toc.state.svelte.ts

packages/common/src/lib/widgets/toc/toc.state.svelte.ts
import type { ApiSublayer } from '$lib/api/layers';
import type { WidgetStateProps } from '$lib/api/managers/widget/widget-declaration';
import type { ApiMapService } from '$lib/api/mapservices';
import { getContext, setContext } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
export class TocState {
readonly states = new Map<ApiMapService | ApiSublayer, TocItemState>();
readonly highlighted = $state(new SvelteMap<string, boolean>());
basemapSelectorOpen = $state(false);
constructor(private readonly props: WidgetStateProps) {}
public get(service: ApiMapService | ApiSublayer) {
if (this.states.has(service)) {
return this.states.get(service)!;
}
const state = new TocItemState(service);
this.states.set(service, state);
return state;
}
expandService(serviceId: string) {
const entry = [...this.states.entries()].find(([service]) => service.id === serviceId);
if (entry) {
const [_, state] = entry;
state.open = true;
}
}
highlightService(serviceId: string, durationInMs: number = 3000): void {
this.highlighted.set(serviceId, true);
setTimeout(() => {
this.highlighted.delete(serviceId);
}, durationInMs);
}
openBasemapSelector() {
this.basemapSelectorOpen = true;
}
closeBasemapSelector() {
this.basemapSelectorOpen = false;
}
}
export class TocItemState {
_showActions = $state(false);
showInfo = $state(false);
showOpacity = $state(false);
get open() {
return this.service.toc.open;
}
set open(value: boolean) {
this.service.toc.open = value;
}
constructor(public readonly service: ApiMapService | ApiSublayer) {}
get showActions() {
return this._showActions;
}
set showActions(v: boolean) {
if (!v) {
this.showInfo = false;
this.showOpacity = false;
}
this._showActions = v;
}
}
export function setState(state: TocState) {
setContext('state-toc', state);
}
export function getState() {
const state = getContext<TocState>('state-toc');
if (!state) {
throw new Error('No toc state set in context');
}
return state;
}

packages/common/src/lib/widgets/toc/Toc.svelte

packages/common/src/lib/widgets/toc/Toc.svelte
<script lang="ts">
import { getI18n } from '$lib/api/managers/i18n';
import { getWidgetManager } from '$lib/api/managers/widget';
import { getMapManager } from '$lib/api/map';
import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte';
import { Icon } from '$lib/components/icon';
import { Button } from '$lib/components/shadcn/ui/button';
import { Sortable } from '$lib/components/sortable';
import Trash2 from 'lucide-svelte/icons/trash-2';
import TocItem from './components/TocItem.svelte';
import TocBasemapSelector from './components/TocBasemapSelector.svelte';
import type { TocProps } from './toc.declaration';
import { setState } from './toc.state.svelte';
import { GeoviewerError } from '$lib/api/managers/error';
import type ServiceDataTableWidget from '$lib/widgets/service-data-table/ServiceDataTableWidget.svelte';
const props: TocProps = $props();
const { fullConfig, state: tocState } = props;
const { config } = fullConfig;
setState(tocState);
const i18n = getI18n(fullConfig.i18n);
const mapManager = getMapManager();
const widgetManager = getWidgetManager();
const displayEmptySelectionButton = $derived.by(
() => layerList.list.filter((service) => service.toc.visible).length > 0,
);
let layerList = mapManager.layerList;
let confirmOpened = $state<boolean>(false);
let basemapSelectorOpen = $state<boolean>(false);
export function highlightService(serviceId: string, durationInMs: number = 3000): void {
tocState.highlightService(serviceId, durationInMs);
}
export function expandService(serviceId: string): void {
tocState.expandService(serviceId);
}
export function openBasemapSelector() {
basemapSelectorOpen = true;
}
export function closeBasemapSelector() {
basemapSelectorOpen = false;
}
function openAddData() {
widgetManager.getReference(config.addDataWidgetId).activate();
}
function emptySelection() {
confirmOpened = false;
closeAllServiceDataTables();
layerList.removeAll();
}
async function closeAllServiceDataTables() {
if (!config.dataTableWidgetId) {
throw new GeoviewerError('No dataTableWidgetId provided');
}
const dataTableWidget = await widgetManager
.getReference<ServiceDataTableWidget>(config.dataTableWidgetId)
.activate();
dataTableWidget.closeAllPanels();
}
</script>
<div class="gv-h-full gv-flex gv-flex-col gv-min-h-0">
{#if !basemapSelectorOpen}
{#if config.showAddDataButton}
<div class="gv-px-5 gv-py-2.5 gv-border-b">
<Button onclick={openAddData} data-test-id="Toc-AddDataButton" size="sm">
<Icon class="gv-size-4 gv-align-baseline" icon={{ geoviewer: 'add-layer' }} />
{i18n('add-data')}
</Button>
</div>
{/if}
<div class="gv-flex gv-flex-col gv-flex-1 gv-overflow-y-auto">
<div class="gv-overflow-y-auto">
<Sortable items={layerList.list} getId={(s) => s.id} onSort={(ids) => layerList.sort(ids)} reversed>
{#snippet children({ item: service, bindDragTrigger })}
{#if service.toc.visible}
{@const state = tocState.get(service)}
<TocItem
bind:headerElement={() => {}, bindDragTrigger}
isRoot
{state}
bind:open={state.open}
highlight={tocState.highlighted.has(service.id)}
{config}
i18nConfig={fullConfig.i18n}
{layerList}
placeholder={i18n('layer-label-empty')}
class="gv-w-full gv-border-b"
/>
{/if}
{/snippet}
</Sortable>
</div>
{#if config.showClearAllButton && displayEmptySelectionButton}
<div class="gv-flex gv-justify-center gv-p-3 gv-bottom-0 gv-w-full gv-bg-background">
<Button
variant="link"
class="gv-text-destructive hover:gv-no-underline"
onclick={() => (confirmOpened = true)}
data-test-id="Toc-EmptySelectionButton"
>
<Trash2 class="gv-size-4" />
{i18n('empty-selection')}
</Button>
</div>
{/if}
</div>
{/if}
{#if config.showBasemapSelector}
<TocBasemapSelector bind:open={basemapSelectorOpen} i18nRegistry={fullConfig.i18n} />
{/if}
</div>
<ConfirmDialog message={i18n('empty-selection-confirmation')} bind:open={confirmOpened}>
{#snippet footer()}
<Button
onclick={() => (confirmOpened = false)}
variant="outline"
data-test-id="Toc-EmptySelectionButton-Cancel"
>
{i18n('common.cancel')}
</Button>
<Button onclick={emptySelection} data-test-id="Toc-EmptySelectionButton-Confirm" variant="destructive">
<Trash2 class="gv-size-4" />
{i18n('common.confirm')}
</Button>
{/snippet}
</ConfirmDialog>

packages/common/src/lib/widgets/toc/toc.topic.ts

packages/common/src/lib/widgets/toc/toc.topic.ts
import type { ApiMapService } from '$lib/api/mapservices';
import type { ApiSublayer } from '$lib/api/layers';
export type TocTopicEvent =
| {
type: 'Toc-Layer-visibility-toggle';
toggle: boolean;
layer: ApiMapService | ApiSublayer;
}
| {
type: 'Toc-Layer-remove';
layer: ApiMapService | ApiSublayer;
}
| {
type: 'Toc-Layer-description-open';
layer: ApiMapService;
}
| {
type: 'Toc-Layer-metadata-open';
layer: ApiMapService;
};

packages/common/src/lib/widgets/toc/toc.utils.ts

packages/common/src/lib/widgets/toc/toc.utils.ts
import type Toc from '$lib/widgets/toc/Toc.svelte';
import type WidgetManager from '$lib/api/managers/widget/widget.manager.svelte';
import type { MapManager } from '$lib/api/map';
import type { TocState } from '$lib/widgets/toc/toc.state.svelte';
export function highlightServiceInToc(
mapServiceId: string,
widgetManager: WidgetManager,
mapManager: MapManager,
): void {
const service = mapManager.layerList.list.find((layer) => layer.id === mapServiceId);
if (service) {
service.onLoad(() => {
const [tocReference] = widgetManager.getReferencesByWidgetClass<Toc, TocState>('Toc');
if (tocReference) {
tocReference.state.highlightService(mapServiceId);
tocReference
.activate({ openContainer: true })
.catch((err) => console.error('Failed on activate toc', err));
}
});
}
}
export function openServiceInToc(mapServiceId: string, widgetManager: WidgetManager): void {
const [tocReference] = widgetManager.getReferencesByWidgetClass<Toc, TocState>('Toc');
if (tocReference) {
tocReference.activate().catch((err) => console.error('Failed on activate toc', err));
tocReference.state.expandService(mapServiceId);
}
}

Aller plus loin