web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

carousel.ts (8675B)


      1 import { get_store } from "$lib/utils/app";
      2 import { exe_iter } from "@radroots/utils";
      3 import { writable, type Writable } from "svelte/store";
      4 
      5 const CAROUSEL_DELAY_MS = 150;
      6 export const CAROUSEL_CONTEXT_KEY = "radroots:carousel";
      7 
      8 export type CarouselStore<T extends string> = {
      9     view: T;
     10     container: Writable<HTMLElement | null>;
     11     item: Writable<HTMLElement | null>;
     12     index: Writable<number>;
     13     max_index: Writable<number>;
     14     step_count: Writable<number>;
     15     wrap: Writable<boolean>;
     16     active: Writable<boolean>;
     17     duration_ms: Writable<number>;
     18 };
     19 
     20 export type CarouselInit<T extends string> = {
     21     view: T;
     22     index?: number;
     23     max_index?: number;
     24     step_count?: number;
     25     wrap?: boolean;
     26     duration_ms?: number;
     27 };
     28 
     29 const max_index_safe = (max_index: number): number => Math.max(max_index, 0);
     30 
     31 const step_count_safe = (step_count: number): number =>
     32     Math.max(step_count, 1);
     33 
     34 const duration_safe = (duration_ms: number): number =>
     35     Math.max(duration_ms, 0);
     36 
     37 const carousel_index_normalize = (
     38     index: number,
     39     max_index: number,
     40     wrap: boolean,
     41 ): number => {
     42     const max_index_safe_val = max_index_safe(max_index);
     43     if (max_index_safe_val === 0) return 0;
     44     if (wrap) {
     45         const range = max_index_safe_val + 1;
     46         const mod = index % range;
     47         return mod < 0 ? mod + range : mod;
     48     }
     49     if (index < 0) return 0;
     50     if (index > max_index_safe_val) return max_index_safe_val;
     51     return index;
     52 };
     53 
     54 const carousel_measure_width = <T extends string>(
     55     carousel: CarouselStore<T>,
     56 ): number => {
     57     const item = get_store(carousel.item);
     58     if (item?.clientWidth) return item.clientWidth;
     59     const container = get_store(carousel.container);
     60     return container?.clientWidth ?? 0;
     61 };
     62 
     63 const carousel_scroll_to = <T extends string>(
     64     carousel: CarouselStore<T>,
     65     index: number,
     66 ): void => {
     67     if (typeof window === "undefined") return;
     68     const container = get_store(carousel.container);
     69     if (!container) return;
     70     const width = carousel_measure_width(carousel);
     71     if (!width) return;
     72     requestAnimationFrame(() => {
     73         container.scrollLeft = width * index;
     74     });
     75 };
     76 
     77 const carousel_step = <T extends string>(
     78     carousel: CarouselStore<T>,
     79     delta: number,
     80 ): void => {
     81     const is_active = get_store(carousel.active);
     82     if (is_active) return;
     83     carousel.active.set(true);
     84     const index = get_store(carousel.index);
     85     const max_index = get_store(carousel.max_index);
     86     const wrap = get_store(carousel.wrap);
     87     const next_index = carousel_index_normalize(
     88         index + delta,
     89         max_index,
     90         wrap,
     91     );
     92     carousel.index.set(next_index);
     93     carousel_scroll_to(carousel, next_index);
     94     carousel.active.set(false);
     95 };
     96 
     97 export const carousel_create = <T extends string>(
     98     opts: CarouselInit<T>,
     99 ): CarouselStore<T> => {
    100     return {
    101         view: opts.view,
    102         container: writable<HTMLElement | null>(null),
    103         item: writable<HTMLElement | null>(null),
    104         index: writable<number>(opts.index ?? 0),
    105         max_index: writable<number>(max_index_safe(opts.max_index ?? 0)),
    106         step_count: writable<number>(step_count_safe(opts.step_count ?? 1)),
    107         wrap: writable<boolean>(opts.wrap ?? false),
    108         active: writable<boolean>(false),
    109         duration_ms: writable<number>(
    110             duration_safe(opts.duration_ms ?? CAROUSEL_DELAY_MS),
    111         ),
    112     };
    113 };
    114 
    115 export const carousel_register_container = <T extends string>(
    116     carousel: CarouselStore<T>,
    117     container: HTMLElement | null,
    118 ): (() => void) => {
    119     const current = get_store(carousel.container);
    120     if (current !== container) carousel.container.set(container);
    121     if (container) carousel_sync(carousel);
    122     return () => {
    123         const stored = get_store(carousel.container);
    124         if (stored === container) carousel.container.set(null);
    125     };
    126 };
    127 
    128 export const carousel_register_item = <T extends string>(
    129     carousel: CarouselStore<T>,
    130     item: HTMLElement | null,
    131 ): (() => void) => {
    132     const current = get_store(carousel.item);
    133     if (!current && item) carousel.item.set(item);
    134     if (item) carousel_sync(carousel);
    135     return () => {
    136         const stored = get_store(carousel.item);
    137         if (stored === item) carousel.item.set(null);
    138     };
    139 };
    140 
    141 export const carousel_sync = <T extends string>(
    142     carousel: CarouselStore<T>,
    143 ): void => {
    144     const index = get_store(carousel.index);
    145     const max_index = get_store(carousel.max_index);
    146     const wrap = get_store(carousel.wrap);
    147     const next_index = carousel_index_normalize(
    148         index,
    149         max_index,
    150         wrap,
    151     );
    152     if (next_index !== index) carousel.index.set(next_index);
    153     carousel_scroll_to(carousel, next_index);
    154 };
    155 
    156 export const carousel_watch = <T extends string>(
    157     carousel: CarouselStore<T>,
    158 ): (() => void) => {
    159     const unsubs = [
    160         carousel.index.subscribe(() => carousel_sync(carousel)),
    161         carousel.max_index.subscribe(() => carousel_sync(carousel)),
    162         carousel.wrap.subscribe(() => carousel_sync(carousel)),
    163         carousel.container.subscribe(() => carousel_sync(carousel)),
    164         carousel.item.subscribe(() => carousel_sync(carousel)),
    165     ];
    166     return () => {
    167         for (const unsub of unsubs) unsub();
    168     };
    169 };
    170 
    171 export const carousel_set_index = <T extends string>(
    172     carousel: CarouselStore<T>,
    173     index: number,
    174 ): void => {
    175     const max_index = get_store(carousel.max_index);
    176     const wrap = get_store(carousel.wrap);
    177     const next_index = carousel_index_normalize(index, max_index, wrap);
    178     carousel.index.set(next_index);
    179     carousel_scroll_to(carousel, next_index);
    180 };
    181 
    182 export const carousel_set_max = <T extends string>(
    183     carousel: CarouselStore<T>,
    184     max_index: number,
    185 ): void => {
    186     const next_max = max_index_safe(max_index);
    187     carousel.max_index.set(next_max);
    188     const index = get_store(carousel.index);
    189     const wrap = get_store(carousel.wrap);
    190     const next_index = carousel_index_normalize(index, next_max, wrap);
    191     carousel.index.set(next_index);
    192     carousel_scroll_to(carousel, next_index);
    193 };
    194 
    195 export const carousel_set_count = <T extends string>(
    196     carousel: CarouselStore<T>,
    197     count: number,
    198 ): void => {
    199     const max_index = count > 0 ? count - 1 : 0;
    200     carousel_set_max(carousel, max_index);
    201 };
    202 
    203 export const carousel_set_wrap = <T extends string>(
    204     carousel: CarouselStore<T>,
    205     wrap: boolean,
    206 ): void => {
    207     carousel.wrap.set(wrap);
    208     const index = get_store(carousel.index);
    209     const max_index = get_store(carousel.max_index);
    210     const next_index = carousel_index_normalize(index, max_index, wrap);
    211     carousel.index.set(next_index);
    212     carousel_scroll_to(carousel, next_index);
    213 };
    214 
    215 export const carousel_set_step_count = <T extends string>(
    216     carousel: CarouselStore<T>,
    217     step_count: number,
    218 ): void => {
    219     carousel.step_count.set(step_count_safe(step_count));
    220 };
    221 
    222 export const carousel_set_duration = <T extends string>(
    223     carousel: CarouselStore<T>,
    224     duration_ms: number,
    225 ): void => {
    226     carousel.duration_ms.set(duration_safe(duration_ms));
    227 };
    228 
    229 export const carousel_init = <T extends string>(
    230     carousel: CarouselStore<T>,
    231     opts?: Omit<CarouselInit<T>, "view">,
    232 ): void => {
    233     if (!opts) return;
    234     if (opts.wrap !== undefined) carousel.wrap.set(opts.wrap);
    235     if (opts.step_count !== undefined)
    236         carousel_set_step_count(carousel, opts.step_count);
    237     if (opts.duration_ms !== undefined)
    238         carousel_set_duration(carousel, opts.duration_ms);
    239     if (opts.max_index !== undefined)
    240         carousel.max_index.set(max_index_safe(opts.max_index));
    241     const wrap = get_store(carousel.wrap);
    242     const max_index = get_store(carousel.max_index);
    243     const index = opts.index ?? get_store(carousel.index);
    244     const next_index = carousel_index_normalize(index, max_index, wrap);
    245     carousel.index.set(next_index);
    246     carousel_scroll_to(carousel, next_index);
    247 };
    248 
    249 export const carousel_inc = async <T extends string>(
    250     carousel: CarouselStore<T>,
    251 ): Promise<void> => {
    252     const step_count = get_store(carousel.step_count);
    253     const duration_ms = get_store(carousel.duration_ms);
    254     await exe_iter(
    255         async () => {
    256             carousel_step(carousel, 1);
    257         },
    258         step_count,
    259         duration_ms,
    260     );
    261 };
    262 
    263 export const carousel_dec = async <T extends string>(
    264     carousel: CarouselStore<T>,
    265 ): Promise<void> => {
    266     const step_count = get_store(carousel.step_count);
    267     const duration_ms = get_store(carousel.duration_ms);
    268     await exe_iter(
    269         async () => {
    270             carousel_step(carousel, -1);
    271         },
    272         step_count,
    273         duration_ms,
    274     );
    275 };