@mxfb/tools 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/Date/index.d.ts +45 -0
- package/components/Date/index.js +65 -0
- package/components/Date/styles.module.css +0 -0
- package/components/Date/types.d.ts +39 -0
- package/components/Date/types.js +37 -0
- package/components/Embed/SpotifyEmbed/index.d.ts +13 -0
- package/components/Embed/SpotifyEmbed/index.js +38 -0
- package/components/Embed/SpotifyEmbed/styles.module.css +0 -0
- package/components/Embed/VimeoEmbed/index.d.ts +14 -0
- package/components/Embed/VimeoEmbed/index.js +40 -0
- package/components/Embed/VimeoEmbed/styles.module.css +0 -0
- package/components/Embed/YoutubeEmbed/index.d.ts +18 -0
- package/components/Embed/YoutubeEmbed/index.js +45 -0
- package/components/Embed/YoutubeEmbed/styles.module.css +0 -0
- package/components/Embed/index.d.ts +44 -0
- package/components/Embed/index.js +71 -0
- package/components/Embed/styles.module.css +0 -0
- package/components/Embed/types.d.ts +22 -0
- package/components/Embed/types.js +7 -0
- package/components/JsonEditor/index.d.ts +227 -0
- package/components/JsonEditor/index.js +281 -0
- package/components/JsonEditor/styles.module.css +0 -0
- package/components/JsonEditor/types.d.ts +5 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +3 -0
- package/components/JsonEditor/utils.js +18 -0
- package/components/ListLoader/index.controlled.d.ts +87 -0
- package/components/ListLoader/index.controlled.js +108 -0
- package/components/ListLoader/index.d.ts +76 -0
- package/components/ListLoader/index.js +162 -0
- package/components/ListLoader/styles.module.css +0 -0
- package/components/index.d.ts +4 -0
- package/components/index.js +4 -0
- package/components/public-classnames.d.ts +6 -0
- package/components/public-classnames.js +6 -0
- package/package.json +64 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { integersBetween } from '@design-edito/tools/agnostic/arrays/integers-between/index.js';
|
|
3
|
+
import { clss } from '@design-edito/tools/agnostic/css/clss/index.js';
|
|
4
|
+
import { IntersectionObserverComponent } from '@design-edito/tools/components/IntersectionObserver/index.js';
|
|
5
|
+
import { listLoader as publicClassName } from '../public-classnames.js';
|
|
6
|
+
import { mergeClassNames } from '../utils/index.js';
|
|
7
|
+
import cssModule from './styles.module.css';
|
|
8
|
+
/**
|
|
9
|
+
* Presentational layer of the paginated list. Renders already-loaded pages as a
|
|
10
|
+
* single flat item list, and interleaves load buttons wherever a page is missing.
|
|
11
|
+
*
|
|
12
|
+
* This component holds no state and performs no fetching. Loaded pages are sorted
|
|
13
|
+
* by position and flattened, so items appear in page order then in item order,
|
|
14
|
+
* with no per-page wrapper element.
|
|
15
|
+
*
|
|
16
|
+
* ### Load buttons
|
|
17
|
+
* One button is rendered per missing page position:
|
|
18
|
+
* - before the first loaded page, unless it is already `firstPagePos`,
|
|
19
|
+
* - in place of each page missing between two loaded pages,
|
|
20
|
+
* - after the last loaded page, unless it is already `lastPagePos`.
|
|
21
|
+
*
|
|
22
|
+
* ### Root element modifiers
|
|
23
|
+
* - `--loading` — at least one page is being fetched.
|
|
24
|
+
*
|
|
25
|
+
* ### CSS elements
|
|
26
|
+
* - `item` — wraps each rendered item. Carries `data-page`.
|
|
27
|
+
* - `load` — a load button. Rendered empty, so its label belongs in CSS.
|
|
28
|
+
* Carries `data-page` and a `prev`, `next` or `gap` modifier.
|
|
29
|
+
* - `load-observer` — the intersection observer wrapping an auto-loading button.
|
|
30
|
+
* Present only when `autoLoadPrevWhenVisible` or `autoLoadNextWhenVisible` is on.
|
|
31
|
+
* Carries a `prev` or `next` modifier.
|
|
32
|
+
*
|
|
33
|
+
* ### Data attributes on the root element
|
|
34
|
+
* - `data-loading-pages` — comma-separated ascending list of the pages being
|
|
35
|
+
* fetched, e.g. `"1,3,6"`. Absent entirely when nothing is loading.
|
|
36
|
+
*
|
|
37
|
+
* @param props - Component properties.
|
|
38
|
+
* @see {@link Props}
|
|
39
|
+
* @returns A flat list of items with load buttons in place of missing pages.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Deduplication runs after `filter`, so an item hidden by `filter` never evicts
|
|
43
|
+
* an earlier duplicate.
|
|
44
|
+
*/
|
|
45
|
+
export const ControlledListLoader = ({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin }) => {
|
|
46
|
+
const inBoundsPages = pages.filter(pagePos => pagePos >= firstPagePos && pagePos <= lastPagePos);
|
|
47
|
+
const filteredItems = Array.from(itemsPages)
|
|
48
|
+
.filter(([pagePos]) => inBoundsPages.includes(pagePos))
|
|
49
|
+
.sort(([pageA], [pageB]) => pageA - pageB)
|
|
50
|
+
.flatMap(([pagePos, items]) => items.map((item, itemPos) => ({ item, pagePos, itemPos })))
|
|
51
|
+
.filter(({ item }) => filter(item));
|
|
52
|
+
const identifiers = filteredItems.map(({ item }) => getIdentifier?.(item));
|
|
53
|
+
const displayedItems = filteredItems.filter((_, pos) => {
|
|
54
|
+
const identifier = identifiers[pos];
|
|
55
|
+
if (identifier === undefined)
|
|
56
|
+
return true;
|
|
57
|
+
return identifiers.lastIndexOf(identifier) === pos;
|
|
58
|
+
});
|
|
59
|
+
const loadedPages = Array.from(itemsPages.keys())
|
|
60
|
+
.filter(pagePos => inBoundsPages.includes(pagePos))
|
|
61
|
+
.sort((a, b) => a - b);
|
|
62
|
+
const firstLoadedPage = loadedPages[0];
|
|
63
|
+
const lastLoadedPage = loadedPages[loadedPages.length - 1];
|
|
64
|
+
const gapPages = loadedPages.flatMap((pagePos, pos) => {
|
|
65
|
+
const prevPage = loadedPages[pos - 1];
|
|
66
|
+
if (prevPage === undefined)
|
|
67
|
+
return [];
|
|
68
|
+
return integersBetween(prevPage + 1, pagePos - 1);
|
|
69
|
+
});
|
|
70
|
+
const sortedLoadingPages = [...loadingPages ?? []]
|
|
71
|
+
.filter(pagePos => inBoundsPages.includes(pagePos))
|
|
72
|
+
.sort((a, b) => a - b);
|
|
73
|
+
const hasNothing = loadedPages.length === 0 && sortedLoadingPages.length === 0;
|
|
74
|
+
const slots = hasNothing
|
|
75
|
+
? [{
|
|
76
|
+
pagePos: [...inBoundsPages].sort((a, b) => a - b)[0] ?? firstPagePos,
|
|
77
|
+
kind: 'gap'
|
|
78
|
+
}]
|
|
79
|
+
: [
|
|
80
|
+
...firstLoadedPage !== undefined && firstLoadedPage > firstPagePos
|
|
81
|
+
? [{ pagePos: firstLoadedPage - 1, kind: 'prev' }]
|
|
82
|
+
: [],
|
|
83
|
+
...loadedPages.map(pagePos => ({ pagePos, kind: 'items' })),
|
|
84
|
+
...gapPages.map(pagePos => ({ pagePos, kind: 'gap' })),
|
|
85
|
+
...lastLoadedPage !== undefined && lastLoadedPage < lastPagePos
|
|
86
|
+
? [{ pagePos: lastLoadedPage + 1, kind: 'next' }]
|
|
87
|
+
: []
|
|
88
|
+
].sort((a, b) => a.pagePos - b.pagePos);
|
|
89
|
+
const c = clss(publicClassName, { cssModule });
|
|
90
|
+
const rootClss = mergeClassNames(c(null, { loading: sortedLoadingPages.length > 0 }), className);
|
|
91
|
+
const renderLoadButton = (pagePos, kind) => {
|
|
92
|
+
const button = _jsx("button", { className: c('load', kind), "data-page": pagePos, onClick: () => onLoadPageClicked?.(pagePos) }, `load/${pagePos}`);
|
|
93
|
+
const autoLoads = (kind === 'prev' && autoLoadPrevWhenVisible === true)
|
|
94
|
+
|| (kind === 'next' && autoLoadNextWhenVisible === true);
|
|
95
|
+
if (!autoLoads)
|
|
96
|
+
return button;
|
|
97
|
+
return _jsx(IntersectionObserverComponent, { className: c('load-observer', kind), threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: ({ ioEntry }) => {
|
|
98
|
+
if (ioEntry?.isIntersecting !== true)
|
|
99
|
+
return;
|
|
100
|
+
onLoadPageClicked?.(pagePos);
|
|
101
|
+
}, children: button }, `load/${pagePos}`);
|
|
102
|
+
};
|
|
103
|
+
return _jsx("div", { className: rootClss, "data-loading-pages": sortedLoadingPages.length === 0 ? undefined : sortedLoadingPages.join(','), children: slots.map(({ pagePos, kind }) => kind === 'items'
|
|
104
|
+
? displayedItems
|
|
105
|
+
.filter(entry => entry.pagePos === pagePos)
|
|
106
|
+
.map(({ item, itemPos }) => _jsx("div", { className: c('item'), "data-page": pagePos, children: display(item) }, getIdentifier?.(item) ?? `${pagePos}/${itemPos}`))
|
|
107
|
+
: renderLoadButton(pagePos, kind)) });
|
|
108
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Props as ControlledProps } from './index.controlled.js';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the {@link ListLoader} component.
|
|
5
|
+
*
|
|
6
|
+
* Extends {@link ControlledProps} — minus the data it derives itself — with
|
|
7
|
+
* fetching, staleness and page-set management.
|
|
8
|
+
*
|
|
9
|
+
* @template T - The item type carried by each page.
|
|
10
|
+
*
|
|
11
|
+
* @property pages - Page positions to keep loaded. When provided, the component
|
|
12
|
+
* is fully controlled: load buttons only report clicks and never change the set
|
|
13
|
+
* themselves. When omitted, the set is managed internally.
|
|
14
|
+
* @property defaultPage - Page loaded first in uncontrolled mode. Clamped into
|
|
15
|
+
* `firstPagePos`–`lastPagePos`, and defaults to `firstPagePos`. Ignored when
|
|
16
|
+
* `pages` is provided.
|
|
17
|
+
* @property fillGaps - When `true` (the default), any page missing between the
|
|
18
|
+
* lowest and highest requested page is requested automatically, so the loaded
|
|
19
|
+
* range stays contiguous. Ignored when `pages` is provided.
|
|
20
|
+
* @property dropPagesFurtherThan - Maximum distance, in pages, kept around a page
|
|
21
|
+
* loaded through a load button. Pages further away are dropped from state and
|
|
22
|
+
* from the DOM. Applies to load-button activations only, never to retries or
|
|
23
|
+
* stale reloads. Ignored when `pages` is provided.
|
|
24
|
+
* @property fetch - Fetches one page's items. Rejections are reported through
|
|
25
|
+
* `onPageFetchFailed` and retried according to `fetchRetriesNb`.
|
|
26
|
+
* @property staleAfterMs - Delay after which a loaded page is refetched, counted
|
|
27
|
+
* from its own last successful load. When omitted, pages are never refreshed.
|
|
28
|
+
* @property onPageFetched - Called with `pagePos` and `items` after a page's items
|
|
29
|
+
* have been stored. Not called for a page dropped while its request was in flight.
|
|
30
|
+
* @property onPageFetchFailed - Called with `pagePos` and `error` on every failed
|
|
31
|
+
* attempt, not only once retries are exhausted. When omitted, failures are logged
|
|
32
|
+
* with `console.warn`.
|
|
33
|
+
* @property fetchRetriesNb - Number of retries after a failed fetch. Defaults to
|
|
34
|
+
* `Infinity`, so a page keeps retrying until it succeeds.
|
|
35
|
+
* @property fetchRetriesDelayMs - Delay between two attempts. Defaults to `1000`.
|
|
36
|
+
*/
|
|
37
|
+
export type Props<T> = Omit<ControlledProps<T>, 'itemsPages' | 'loadingPages' | 'pages'> & {
|
|
38
|
+
pages?: number[];
|
|
39
|
+
defaultPage?: number;
|
|
40
|
+
fillGaps?: boolean;
|
|
41
|
+
dropPagesFurtherThan?: number;
|
|
42
|
+
fetch: (page: number) => Promise<T[]>;
|
|
43
|
+
staleAfterMs?: number;
|
|
44
|
+
onPageFetched?: (payload: {
|
|
45
|
+
pagePos: number;
|
|
46
|
+
items: T[];
|
|
47
|
+
}) => void;
|
|
48
|
+
onPageFetchFailed?: (payload: {
|
|
49
|
+
pagePos: number;
|
|
50
|
+
error: Error;
|
|
51
|
+
}) => void;
|
|
52
|
+
fetchRetriesNb?: number;
|
|
53
|
+
fetchRetriesDelayMs?: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Paginated list abstraction. Fetches the pages it is asked for, keeps them
|
|
57
|
+
* fresh, and drives a {@link ControlledListLoader} with the result.
|
|
58
|
+
*
|
|
59
|
+
* @param props - Component properties.
|
|
60
|
+
* @see {@link Props}
|
|
61
|
+
* @see {@link ControlledListLoader} for the rendered markup and CSS elements.
|
|
62
|
+
* @returns A {@link ControlledListLoader} fed with the loaded pages.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* - In controlled mode (`pages` defined), the page set is entirely driven by the
|
|
66
|
+
* parent. `fillGaps` and `dropPagesFurtherThan` are inert, and load buttons only
|
|
67
|
+
* report through `onLoadPageClicked`.
|
|
68
|
+
* - In uncontrolled mode, internal state is initialized from `defaultPage` and load
|
|
69
|
+
* buttons extend the set themselves. `onLoadPageClicked` still fires, after the
|
|
70
|
+
* internal state has been updated.
|
|
71
|
+
* - A page removed from the effective set is dropped from memory, and a request
|
|
72
|
+
* still in flight for it is discarded on arrival rather than re-inserted.
|
|
73
|
+
* - No page outside `firstPagePos`–`lastPagePos` is ever fetched, whichever mode
|
|
74
|
+
* is in use.
|
|
75
|
+
*/
|
|
76
|
+
export declare const ListLoader: <T>({ className, pages, defaultPage, fillGaps, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb, fetchRetriesDelayMs }: Props<T>) => ReactNode;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { integersBetween } from '@design-edito/tools/agnostic/arrays/integers-between/index.js';
|
|
4
|
+
import { toError } from '@design-edito/tools/agnostic/misc/cast/index.js';
|
|
5
|
+
import { clamp } from '@design-edito/tools/agnostic/numbers/clamp/index.js';
|
|
6
|
+
import { ControlledListLoader } from './index.controlled.js';
|
|
7
|
+
/**
|
|
8
|
+
* Paginated list abstraction. Fetches the pages it is asked for, keeps them
|
|
9
|
+
* fresh, and drives a {@link ControlledListLoader} with the result.
|
|
10
|
+
*
|
|
11
|
+
* @param props - Component properties.
|
|
12
|
+
* @see {@link Props}
|
|
13
|
+
* @see {@link ControlledListLoader} for the rendered markup and CSS elements.
|
|
14
|
+
* @returns A {@link ControlledListLoader} fed with the loaded pages.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* - In controlled mode (`pages` defined), the page set is entirely driven by the
|
|
18
|
+
* parent. `fillGaps` and `dropPagesFurtherThan` are inert, and load buttons only
|
|
19
|
+
* report through `onLoadPageClicked`.
|
|
20
|
+
* - In uncontrolled mode, internal state is initialized from `defaultPage` and load
|
|
21
|
+
* buttons extend the set themselves. `onLoadPageClicked` still fires, after the
|
|
22
|
+
* internal state has been updated.
|
|
23
|
+
* - A page removed from the effective set is dropped from memory, and a request
|
|
24
|
+
* still in flight for it is discarded on arrival rather than re-inserted.
|
|
25
|
+
* - No page outside `firstPagePos`–`lastPagePos` is ever fetched, whichever mode
|
|
26
|
+
* is in use.
|
|
27
|
+
*/
|
|
28
|
+
export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb = Infinity, fetchRetriesDelayMs = 1000 }) => {
|
|
29
|
+
const [itemsPages, setItemsPages] = useState(new Map());
|
|
30
|
+
const [loadingPages, setLoadingPages] = useState(new Set());
|
|
31
|
+
const [internalPages, setInternalPages] = useState([
|
|
32
|
+
clamp(defaultPage ?? firstPagePos, firstPagePos, lastPagePos)
|
|
33
|
+
]);
|
|
34
|
+
const requestedPages = useRef(new Set());
|
|
35
|
+
const retryTimeouts = useRef(new Set());
|
|
36
|
+
// Single source of truth for what should be loaded. Memoized so the effects
|
|
37
|
+
// below keep a stable dependency when the page set has not actually changed.
|
|
38
|
+
const currentPages = useMemo(() => (pages ?? internalPages).filter(page => page >= firstPagePos && page <= lastPagePos), [pages, internalPages, firstPagePos, lastPagePos]);
|
|
39
|
+
const handleLoadClick = (pagePos) => {
|
|
40
|
+
if (pages === undefined) {
|
|
41
|
+
setInternalPages(curr => {
|
|
42
|
+
const next = curr.includes(pagePos) ? curr : [...curr, pagePos];
|
|
43
|
+
if (dropPagesFurtherThan === undefined)
|
|
44
|
+
return next;
|
|
45
|
+
const kept = next.filter(page => Math.abs(page - pagePos) <= dropPagesFurtherThan);
|
|
46
|
+
return kept.length === next.length ? next : kept;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const gaveUp = requestedPages.current.has(pagePos)
|
|
50
|
+
&& !itemsPages.has(pagePos)
|
|
51
|
+
&& !loadingPages.has(pagePos);
|
|
52
|
+
if (gaveUp)
|
|
53
|
+
loadPage(pagePos);
|
|
54
|
+
onLoadPageClicked?.(pagePos);
|
|
55
|
+
};
|
|
56
|
+
const storePage = (page, items) => setItemsPages(curr => {
|
|
57
|
+
const next = new Map(curr);
|
|
58
|
+
next.set(page, { loadedAt: new Date(), items });
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
const setPageLoading = (page, isLoading) => setLoadingPages(curr => {
|
|
62
|
+
const next = new Set(curr);
|
|
63
|
+
if (isLoading)
|
|
64
|
+
next.add(page);
|
|
65
|
+
else
|
|
66
|
+
next.delete(page);
|
|
67
|
+
return next;
|
|
68
|
+
});
|
|
69
|
+
const reportFetchError = (page, err) => {
|
|
70
|
+
const error = toError(err);
|
|
71
|
+
if (onPageFetchFailed !== undefined)
|
|
72
|
+
return onPageFetchFailed({ pagePos: page, error });
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.warn(`ListLoader failed to fetch page ${page}`, error);
|
|
75
|
+
};
|
|
76
|
+
// Guards the whole load path at once: a page dropped meanwhile stops its
|
|
77
|
+
// pending retries and its stale reloads without any further bookkeeping.
|
|
78
|
+
const loadPage = (page, retriesLeft = fetchRetriesNb) => {
|
|
79
|
+
if (!requestedPages.current.has(page))
|
|
80
|
+
return;
|
|
81
|
+
setPageLoading(page, true);
|
|
82
|
+
void fetch(page)
|
|
83
|
+
.then(items => {
|
|
84
|
+
if (!requestedPages.current.has(page))
|
|
85
|
+
return;
|
|
86
|
+
storePage(page, items);
|
|
87
|
+
onPageFetched?.({ pagePos: page, items });
|
|
88
|
+
setPageLoading(page, false);
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
reportFetchError(page, err);
|
|
92
|
+
if (retriesLeft <= 0)
|
|
93
|
+
return setPageLoading(page, false);
|
|
94
|
+
const timeout = window.setTimeout(() => {
|
|
95
|
+
retryTimeouts.current.delete(timeout);
|
|
96
|
+
loadPage(page, retriesLeft - 1);
|
|
97
|
+
}, fetchRetriesDelayMs);
|
|
98
|
+
retryTimeouts.current.add(timeout);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
// Fx. dep. none - Emptying `requestedPages` is what stops a fetch already in flight.
|
|
102
|
+
useEffect(() => () => {
|
|
103
|
+
retryTimeouts.current.forEach(timeout => { window.clearTimeout(timeout); });
|
|
104
|
+
retryTimeouts.current.clear();
|
|
105
|
+
requestedPages.current.clear();
|
|
106
|
+
}, []);
|
|
107
|
+
// Fx. dep. `currentPages` - Drops everything held for pages no longer wanted.
|
|
108
|
+
// The updaters return `curr` untouched when nothing was removed, so an unstable
|
|
109
|
+
// `pages` prop cannot spin the render loop.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
Array.from(requestedPages.current)
|
|
112
|
+
.filter(page => !currentPages.includes(page))
|
|
113
|
+
.forEach(page => { requestedPages.current.delete(page); });
|
|
114
|
+
setItemsPages(curr => {
|
|
115
|
+
const next = new Map(Array.from(curr).filter(([page]) => currentPages.includes(page)));
|
|
116
|
+
return next.size === curr.size ? curr : next;
|
|
117
|
+
});
|
|
118
|
+
setLoadingPages(curr => {
|
|
119
|
+
const next = new Set(Array.from(curr).filter(page => currentPages.includes(page)));
|
|
120
|
+
return next.size === curr.size ? curr : next;
|
|
121
|
+
});
|
|
122
|
+
}, [currentPages]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (pages !== undefined)
|
|
125
|
+
return;
|
|
126
|
+
if (!fillGaps)
|
|
127
|
+
return;
|
|
128
|
+
setInternalPages(curr => {
|
|
129
|
+
if (curr.length === 0)
|
|
130
|
+
return curr;
|
|
131
|
+
const missing = integersBetween(Math.min(...curr), Math.max(...curr))
|
|
132
|
+
.filter(page => !curr.includes(page));
|
|
133
|
+
if (missing.length === 0)
|
|
134
|
+
return curr;
|
|
135
|
+
return [...curr, ...missing];
|
|
136
|
+
});
|
|
137
|
+
}, [currentPages, fillGaps, pages]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
currentPages.forEach(page => {
|
|
140
|
+
if (requestedPages.current.has(page))
|
|
141
|
+
return;
|
|
142
|
+
requestedPages.current.add(page);
|
|
143
|
+
loadPage(page);
|
|
144
|
+
});
|
|
145
|
+
}, [currentPages, fetch]);
|
|
146
|
+
// Fx. dep. `itemsPages` - Reschedules one timeout per page on every store, each
|
|
147
|
+
// due from its own `loadedAt`. A refetch updates `loadedAt`, which re-runs this
|
|
148
|
+
// effect and keeps the cycle going.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (staleAfterMs === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const timeouts = Array.from(itemsPages).map(([page, { loadedAt }]) => {
|
|
153
|
+
const dueInMs = Math.max(0, staleAfterMs - (Date.now() - loadedAt.getTime()));
|
|
154
|
+
return window.setTimeout(() => loadPage(page), dueInMs);
|
|
155
|
+
});
|
|
156
|
+
return () => timeouts.forEach(timeout => window.clearTimeout(timeout));
|
|
157
|
+
}, [itemsPages, staleAfterMs, fetch]);
|
|
158
|
+
const controlledItemsPages = new Map(Array
|
|
159
|
+
.from(itemsPages)
|
|
160
|
+
.map(([page, { items }]) => [page, items]));
|
|
161
|
+
return _jsx(ControlledListLoader, { className: className, pages: currentPages, firstPagePos: firstPagePos, lastPagePos: lastPagePos, itemsPages: controlledItemsPages, filter: filter, display: display, getIdentifier: getIdentifier, loadingPages: Array.from(loadingPages), onLoadPageClicked: handleLoadClick, autoLoadPrevWhenVisible: autoLoadPrevWhenVisible, autoLoadNextWhenVisible: autoLoadNextWhenVisible, threshold: threshold, root: root, rootMargin: rootMargin });
|
|
162
|
+
};
|
|
File without changes
|
package/components/index.d.ts
CHANGED
package/components/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export declare const button = "mxfb-button";
|
|
2
|
+
export declare const date = "mxfb-date";
|
|
3
|
+
export declare const embed = "mxfb-embed";
|
|
2
4
|
export declare const input = "mxfb-input";
|
|
3
5
|
export declare const jsonEditor = "mxfb-json-editor";
|
|
6
|
+
export declare const listLoader = "mxfb-list-loader";
|
|
4
7
|
export declare const select = "mxfb-select";
|
|
8
|
+
export declare const spotifyEmbed = "mxfb-spotify-embed";
|
|
5
9
|
export declare const textarea = "mxfb-textarea";
|
|
10
|
+
export declare const vimeoEmbed = "mxfb-vimeo-embed";
|
|
11
|
+
export declare const youtubeEmbed = "mxfb-youtube-embed";
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const button = 'mxfb-button';
|
|
2
|
+
export const date = 'mxfb-date';
|
|
3
|
+
export const embed = 'mxfb-embed';
|
|
2
4
|
export const input = 'mxfb-input';
|
|
3
5
|
export const jsonEditor = 'mxfb-json-editor';
|
|
6
|
+
export const listLoader = 'mxfb-list-loader';
|
|
4
7
|
export const select = 'mxfb-select';
|
|
8
|
+
export const spotifyEmbed = 'mxfb-spotify-embed';
|
|
5
9
|
export const textarea = 'mxfb-textarea';
|
|
10
|
+
export const vimeoEmbed = 'mxfb-vimeo-embed';
|
|
11
|
+
export const youtubeEmbed = 'mxfb-youtube-embed';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxfb/tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Maxime Fabas",
|
|
6
6
|
"license": "ISC",
|
|
@@ -116,9 +116,70 @@
|
|
|
116
116
|
"import": "./components/index.js",
|
|
117
117
|
"types": "./components/index.d.ts"
|
|
118
118
|
},
|
|
119
|
+
"./components/Date": {
|
|
120
|
+
"import": "./components/Date/index.js",
|
|
121
|
+
"types": "./components/Date/index.d.ts"
|
|
122
|
+
},
|
|
123
|
+
"./components/Date/index.js": {
|
|
124
|
+
"import": "./components/Date/index.js"
|
|
125
|
+
},
|
|
126
|
+
"./components/Date/types.js": {
|
|
127
|
+
"import": "./components/Date/types.js",
|
|
128
|
+
"types": "./components/Date/types.d.ts"
|
|
129
|
+
},
|
|
130
|
+
"./components/Embed": {
|
|
131
|
+
"import": "./components/Embed/index.js",
|
|
132
|
+
"types": "./components/Embed/index.d.ts"
|
|
133
|
+
},
|
|
134
|
+
"./components/Embed/index.js": {
|
|
135
|
+
"import": "./components/Embed/index.js"
|
|
136
|
+
},
|
|
137
|
+
"./components/Embed/SpotifyEmbed": {
|
|
138
|
+
"import": "./components/Embed/SpotifyEmbed/index.js",
|
|
139
|
+
"types": "./components/Embed/SpotifyEmbed/index.d.ts"
|
|
140
|
+
},
|
|
141
|
+
"./components/Embed/SpotifyEmbed/index.js": {
|
|
142
|
+
"import": "./components/Embed/SpotifyEmbed/index.js"
|
|
143
|
+
},
|
|
144
|
+
"./components/Embed/types.js": {
|
|
145
|
+
"import": "./components/Embed/types.js",
|
|
146
|
+
"types": "./components/Embed/types.d.ts"
|
|
147
|
+
},
|
|
148
|
+
"./components/Embed/VimeoEmbed": {
|
|
149
|
+
"import": "./components/Embed/VimeoEmbed/index.js",
|
|
150
|
+
"types": "./components/Embed/VimeoEmbed/index.d.ts"
|
|
151
|
+
},
|
|
152
|
+
"./components/Embed/VimeoEmbed/index.js": {
|
|
153
|
+
"import": "./components/Embed/VimeoEmbed/index.js"
|
|
154
|
+
},
|
|
155
|
+
"./components/Embed/YoutubeEmbed": {
|
|
156
|
+
"import": "./components/Embed/YoutubeEmbed/index.js",
|
|
157
|
+
"types": "./components/Embed/YoutubeEmbed/index.d.ts"
|
|
158
|
+
},
|
|
159
|
+
"./components/Embed/YoutubeEmbed/index.js": {
|
|
160
|
+
"import": "./components/Embed/YoutubeEmbed/index.js"
|
|
161
|
+
},
|
|
119
162
|
"./components/index.js": {
|
|
120
163
|
"import": "./components/index.js"
|
|
121
164
|
},
|
|
165
|
+
"./components/JsonEditor": {
|
|
166
|
+
"import": "./components/JsonEditor/index.js",
|
|
167
|
+
"types": "./components/JsonEditor/index.d.ts"
|
|
168
|
+
},
|
|
169
|
+
"./components/JsonEditor/index.js": {
|
|
170
|
+
"import": "./components/JsonEditor/index.js"
|
|
171
|
+
},
|
|
172
|
+
"./components/JsonEditor/types.js": {
|
|
173
|
+
"import": "./components/JsonEditor/types.js",
|
|
174
|
+
"types": "./components/JsonEditor/types.d.ts"
|
|
175
|
+
},
|
|
176
|
+
"./components/ListLoader": {
|
|
177
|
+
"import": "./components/ListLoader/index.js",
|
|
178
|
+
"types": "./components/ListLoader/index.d.ts"
|
|
179
|
+
},
|
|
180
|
+
"./components/ListLoader/index.js": {
|
|
181
|
+
"import": "./components/ListLoader/index.js"
|
|
182
|
+
},
|
|
122
183
|
"./components/utils": {
|
|
123
184
|
"import": "./components/utils/index.js",
|
|
124
185
|
"types": "./components/utils/index.d.ts"
|
|
@@ -174,7 +235,7 @@
|
|
|
174
235
|
"@aws-sdk/client-s3": "^3.999.0",
|
|
175
236
|
"@aws-sdk/lib-storage": "^3.1008.0",
|
|
176
237
|
"@design-edito/publisher": "^0.1.17",
|
|
177
|
-
"@design-edito/tools": "^0.5.
|
|
238
|
+
"@design-edito/tools": "^0.5.42",
|
|
178
239
|
"@google-cloud/storage": "^7.19.0",
|
|
179
240
|
"@inquirer/prompts": "^8.3.0",
|
|
180
241
|
"ansi-regex": "^6.2.2",
|
|
@@ -221,6 +282,7 @@
|
|
|
221
282
|
"eslint-config-love": "^154.1.0",
|
|
222
283
|
"globals": "^17.11.0",
|
|
223
284
|
"prompts": "^2.4.2",
|
|
285
|
+
"sass": "^1.107.1",
|
|
224
286
|
"semver": "^7.7.4",
|
|
225
287
|
"simple-git": "^3.33.0",
|
|
226
288
|
"typescript": "^5.3.3",
|