element-book 26.12.1 → 26.13.0
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/dist/data/book-entry/book-entry.d.ts +1 -1
- package/dist/data/book-entry/book-entry.js +1 -1
- package/dist/data/book-entry/book-page/book-page-controls.d.ts +2 -2
- package/dist/data/book-entry/book-page/book-page-controls.js +2 -2
- package/dist/data/book-entry/book-page/define-book-page.js +2 -0
- package/dist/data/book-entry/url-breadcrumbs.d.ts +3 -3
- package/dist/data/book-entry/url-breadcrumbs.js +3 -3
- package/dist/data/book-entry/verify-book-entry.d.ts +1 -0
- package/dist/data/book-entry/verify-book-entry.js +15 -2
- package/dist/ui/elements/entry-display/entry-display/book-lazy-entry.element.d.ts +13 -0
- package/dist/ui/elements/entry-display/entry-display/book-lazy-entry.element.js +115 -0
- package/dist/ui/elements/entry-display/entry-display/create-node-templates.js +13 -2
- package/package.json +8 -7
|
@@ -10,7 +10,7 @@ export type BookEntry = BookPage | BookRoot | BookElementExample;
|
|
|
10
10
|
/**
|
|
11
11
|
* Check if the input is a book entry of the given type.
|
|
12
12
|
*
|
|
13
|
-
* @
|
|
13
|
+
* @category Internal
|
|
14
14
|
*/
|
|
15
15
|
export declare function isBookEntry<const SpecificType extends BookEntryType>(entry: unknown, entryType: SpecificType): entry is Extract<BookEntry, {
|
|
16
16
|
entryType: SpecificType;
|
|
@@ -2,7 +2,7 @@ import { check } from '@augment-vir/assert';
|
|
|
2
2
|
/**
|
|
3
3
|
* Check if the input is a book entry of the given type.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @category Internal
|
|
6
6
|
*/
|
|
7
7
|
export function isBookEntry(entry, entryType) {
|
|
8
8
|
return check.hasKey(entry, 'entryType') && entry.entryType === entryType;
|
|
@@ -21,7 +21,7 @@ export type BookPageControlInit<ControlType extends BookPageControlType> = Omit<
|
|
|
21
21
|
/**
|
|
22
22
|
* Checks and type guards that the input page control init is of the given type.
|
|
23
23
|
*
|
|
24
|
-
* @
|
|
24
|
+
* @category Internal
|
|
25
25
|
*/
|
|
26
26
|
export declare function isControlInitType<const SpecificControlType extends BookPageControlType>(controlInit: BookPageControlInit<any>, specificType: SpecificControlType): controlInit is BookPageControlInit<SpecificControlType>;
|
|
27
27
|
/**
|
|
@@ -87,6 +87,6 @@ export type BookPageControlValueType = typeof controlValueTypes;
|
|
|
87
87
|
/**
|
|
88
88
|
* Checks that the given control init object is valid.
|
|
89
89
|
*
|
|
90
|
-
* @
|
|
90
|
+
* @category Internal
|
|
91
91
|
*/
|
|
92
92
|
export declare function checkControls(controlsInit: BookPageControlsInitBase | undefined, pageName: string): Error[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checks and type guards that the input page control init is of the given type.
|
|
3
3
|
*
|
|
4
|
-
* @
|
|
4
|
+
* @category Internal
|
|
5
5
|
*/
|
|
6
6
|
export function isControlInitType(controlInit, specificType) {
|
|
7
7
|
return controlInit.controlType === specificType;
|
|
@@ -47,7 +47,7 @@ export const controlValueTypes = {
|
|
|
47
47
|
/**
|
|
48
48
|
* Checks that the given control init object is valid.
|
|
49
49
|
*
|
|
50
|
-
* @
|
|
50
|
+
* @category Internal
|
|
51
51
|
*/
|
|
52
52
|
export function checkControls(controlsInit, pageName) {
|
|
53
53
|
if (!controlsInit) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { check } from '@augment-vir/assert';
|
|
3
3
|
import { BookEntryType } from '../book-entry-type.js';
|
|
4
4
|
import { titleToUrlBreadcrumb } from '../url-breadcrumbs.js';
|
|
5
|
+
import { getPageTitleError } from '../verify-book-entry.js';
|
|
5
6
|
/**
|
|
6
7
|
* A variant of {@link defineBookPage} that allows you specify what the expected global element-book
|
|
7
8
|
* values are for the page that you are defining.
|
|
@@ -41,6 +42,7 @@ export function defineBookPage(pageInit) {
|
|
|
41
42
|
errors: [
|
|
42
43
|
alreadyTakenElementExampleNames.has(elementExampleInit.title) &&
|
|
43
44
|
new Error(`Example title '${elementExampleInit.title}' in page '${pageInit.title}' is already taken.`),
|
|
45
|
+
getPageTitleError(elementExampleInit.title),
|
|
44
46
|
].filter(check.isTruthy),
|
|
45
47
|
};
|
|
46
48
|
alreadyTakenElementExampleNames.add(elementExampleInit.title);
|
|
@@ -2,20 +2,20 @@ import { type BookEntry } from './book-entry.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Create a list of URL breadcrumbs for the given element-book entry.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @category Internal
|
|
6
6
|
*/
|
|
7
7
|
export declare function listUrlBreadcrumbs(entry: BookEntry, includeSelf: boolean): string[];
|
|
8
8
|
/**
|
|
9
9
|
* Convert an element-book entry's title to a URL-safe breadcrumb title.
|
|
10
10
|
*
|
|
11
|
-
* @
|
|
11
|
+
* @category Internal
|
|
12
12
|
*/
|
|
13
13
|
export declare function titleToUrlBreadcrumb(title: string): string;
|
|
14
14
|
/**
|
|
15
15
|
* Check if a full list of URL breadcrumbs (`searchIn`) starts with the subset list of URL
|
|
16
16
|
* breadcrumbs (`searchFor`).
|
|
17
17
|
*
|
|
18
|
-
* @
|
|
18
|
+
* @category Internal
|
|
19
19
|
*/
|
|
20
20
|
export declare function doBreadcrumbsStartWith({ searchFor, searchIn, }: {
|
|
21
21
|
searchIn: ReadonlyArray<string>;
|
|
@@ -2,7 +2,7 @@ import { collapseWhiteSpace } from '@augment-vir/common';
|
|
|
2
2
|
/**
|
|
3
3
|
* Create a list of URL breadcrumbs for the given element-book entry.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @category Internal
|
|
6
6
|
*/
|
|
7
7
|
export function listUrlBreadcrumbs(entry, includeSelf) {
|
|
8
8
|
const entryBreadcrumb = titleToUrlBreadcrumb(entry.title);
|
|
@@ -22,7 +22,7 @@ export function listUrlBreadcrumbs(entry, includeSelf) {
|
|
|
22
22
|
/**
|
|
23
23
|
* Convert an element-book entry's title to a URL-safe breadcrumb title.
|
|
24
24
|
*
|
|
25
|
-
* @
|
|
25
|
+
* @category Internal
|
|
26
26
|
*/
|
|
27
27
|
export function titleToUrlBreadcrumb(title) {
|
|
28
28
|
return collapseWhiteSpace(title).toLowerCase().replaceAll(/\s/g, '-');
|
|
@@ -31,7 +31,7 @@ export function titleToUrlBreadcrumb(title) {
|
|
|
31
31
|
* Check if a full list of URL breadcrumbs (`searchIn`) starts with the subset list of URL
|
|
32
32
|
* breadcrumbs (`searchFor`).
|
|
33
33
|
*
|
|
34
|
-
* @
|
|
34
|
+
* @category Internal
|
|
35
35
|
*/
|
|
36
36
|
export function doBreadcrumbsStartWith({ searchFor, searchIn, }) {
|
|
37
37
|
return searchFor.every((breadcrumb, index) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { BookEntryType } from './book-entry-type.js';
|
|
2
2
|
import { type BookEntry } from './book-entry.js';
|
|
3
|
+
export declare function getPageTitleError(title: string): Error | undefined;
|
|
3
4
|
export declare const bookEntryVerifiers: { [EntryType in BookEntryType]: (entry: BookEntry) => Error[]; };
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
2
|
import { BookEntryType } from './book-entry-type.js';
|
|
3
3
|
import { checkControls } from './book-page/book-page-controls.js';
|
|
4
|
+
import { titleToUrlBreadcrumb } from './url-breadcrumbs.js';
|
|
5
|
+
/** Characters that are not allowed in book entry titles because they would break URL routing. */
|
|
6
|
+
const invalidTitleCharacters = /[/?#&=]/;
|
|
7
|
+
export function getPageTitleError(title) {
|
|
8
|
+
const invalidMatch = title.match(invalidTitleCharacters);
|
|
9
|
+
return title.trim()
|
|
10
|
+
? titleToUrlBreadcrumb(title)
|
|
11
|
+
? invalidMatch
|
|
12
|
+
? new Error(`Book page title has invalid character '${invalidMatch[0]}'.`)
|
|
13
|
+
: undefined
|
|
14
|
+
: new Error(`Book page title resolved to empty breadcrumb.`)
|
|
15
|
+
: new Error(`Cannot define an element-book page with an empty title.`);
|
|
16
|
+
}
|
|
4
17
|
export const bookEntryVerifiers = {
|
|
5
18
|
[BookEntryType.ElementExample]: () => {
|
|
6
|
-
|
|
19
|
+
/** Currently all element example checking happens on page definition. */
|
|
7
20
|
return [];
|
|
8
21
|
},
|
|
9
22
|
[BookEntryType.Page]: (bookPage) => {
|
|
10
23
|
return [
|
|
11
|
-
|
|
24
|
+
getPageTitleError(bookPage.title),
|
|
12
25
|
...checkControls(bookPage.controls, bookPage.title),
|
|
13
26
|
].filter(check.isTruthy);
|
|
14
27
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type HtmlInterpolation } from 'element-vir';
|
|
2
|
+
export declare const BookLazyEntry: import("element-vir").DeclarativeElementDefinition<"book-lazy-entry", {
|
|
3
|
+
/**
|
|
4
|
+
* The content to lazily render. This cannot be rendered via a `<slot>` because then they won't
|
|
5
|
+
* be lazily rendered.
|
|
6
|
+
*/
|
|
7
|
+
content: HtmlInterpolation;
|
|
8
|
+
}, {
|
|
9
|
+
/** Whether the content has been rendered (and should stay rendered). */
|
|
10
|
+
hasRendered: boolean;
|
|
11
|
+
/** Reference to the placeholder element for cleanup. */
|
|
12
|
+
placeholderElement: Element | undefined;
|
|
13
|
+
}, {}, "book-lazy-entry-", "book-lazy-entry-", readonly [], readonly []>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { convertDuration } from 'date-vir';
|
|
2
|
+
import { css, html, onDomCreated } from 'element-vir';
|
|
3
|
+
import { defineBookElement } from '../../define-book-element.js';
|
|
4
|
+
/** Debounce time in milliseconds before rendering an entry that becomes visible. */
|
|
5
|
+
const visibilityDebounce = {
|
|
6
|
+
milliseconds: 10,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Shared IntersectionObserver instance for all BookLazyEntry elements. Using a single observer is
|
|
10
|
+
* more performant than creating one per element.
|
|
11
|
+
*/
|
|
12
|
+
let sharedObserver;
|
|
13
|
+
/** Map of observed elements to their callbacks. */
|
|
14
|
+
const observedElements = new Map();
|
|
15
|
+
/** Map of elements to their debounce timeout IDs. */
|
|
16
|
+
const debounceTimeouts = new Map();
|
|
17
|
+
function getSharedObserver() {
|
|
18
|
+
if (!sharedObserver) {
|
|
19
|
+
sharedObserver = new IntersectionObserver((entries) => {
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const element = entry.target;
|
|
22
|
+
const callback = observedElements.get(element);
|
|
23
|
+
if (!callback) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (entry.isIntersecting) {
|
|
27
|
+
// Element became visible - start debounce timer
|
|
28
|
+
if (!debounceTimeouts.has(element)) {
|
|
29
|
+
const timeoutId = globalThis.setTimeout(() => {
|
|
30
|
+
debounceTimeouts.delete(element);
|
|
31
|
+
callback();
|
|
32
|
+
// Stop observing after rendering
|
|
33
|
+
sharedObserver?.unobserve(element);
|
|
34
|
+
observedElements.delete(element);
|
|
35
|
+
}, convertDuration(visibilityDebounce, {
|
|
36
|
+
milliseconds: true,
|
|
37
|
+
}).milliseconds);
|
|
38
|
+
debounceTimeouts.set(element, timeoutId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Element left viewport - cancel debounce if pending
|
|
43
|
+
const existingTimeout = debounceTimeouts.get(element);
|
|
44
|
+
if (existingTimeout) {
|
|
45
|
+
clearTimeout(existingTimeout);
|
|
46
|
+
debounceTimeouts.delete(element);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, {
|
|
51
|
+
/** Use a small margin to start loading slightly before the element is visible. */
|
|
52
|
+
rootMargin: '100px',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return sharedObserver;
|
|
56
|
+
}
|
|
57
|
+
function unobserveElement(element) {
|
|
58
|
+
const timeout = debounceTimeouts.get(element);
|
|
59
|
+
if (timeout) {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
debounceTimeouts.delete(element);
|
|
62
|
+
}
|
|
63
|
+
observedElements.delete(element);
|
|
64
|
+
sharedObserver?.unobserve(element);
|
|
65
|
+
}
|
|
66
|
+
export const BookLazyEntry = defineBookElement()({
|
|
67
|
+
tagName: 'book-lazy-entry',
|
|
68
|
+
state() {
|
|
69
|
+
return {
|
|
70
|
+
/** Whether the content has been rendered (and should stay rendered). */
|
|
71
|
+
hasRendered: false,
|
|
72
|
+
/** Reference to the placeholder element for cleanup. */
|
|
73
|
+
placeholderElement: undefined,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
styles: css `
|
|
77
|
+
:host {
|
|
78
|
+
display: contents;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.placeholder {
|
|
82
|
+
/* Minimum height to ensure the placeholder is observable */
|
|
83
|
+
min-height: 50px;
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
`,
|
|
87
|
+
cleanup({ state }) {
|
|
88
|
+
if (state.placeholderElement) {
|
|
89
|
+
unobserveElement(state.placeholderElement);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
render({ inputs, state, updateState }) {
|
|
93
|
+
if (state.hasRendered) {
|
|
94
|
+
return inputs.content;
|
|
95
|
+
}
|
|
96
|
+
return html `
|
|
97
|
+
<div
|
|
98
|
+
class="placeholder"
|
|
99
|
+
${onDomCreated((element) => {
|
|
100
|
+
// Clean up previous observation if any
|
|
101
|
+
if (state.placeholderElement) {
|
|
102
|
+
unobserveElement(state.placeholderElement);
|
|
103
|
+
}
|
|
104
|
+
updateState({ placeholderElement: element });
|
|
105
|
+
observedElements.set(element, () => {
|
|
106
|
+
updateState({ hasRendered: true });
|
|
107
|
+
});
|
|
108
|
+
getSharedObserver().observe(element);
|
|
109
|
+
})}
|
|
110
|
+
>
|
|
111
|
+
|
|
112
|
+
</div>
|
|
113
|
+
`;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
@@ -8,6 +8,7 @@ import { BookError } from '../../common/book-error.element.js';
|
|
|
8
8
|
import { BookPageControls } from '../book-page/book-page-controls.element.js';
|
|
9
9
|
import { BookPageWrapper } from '../book-page/book-page-wrapper.element.js';
|
|
10
10
|
import { BookElementExampleWrapper } from '../element-example/book-element-example-wrapper.element.js';
|
|
11
|
+
import { BookLazyEntry } from './book-lazy-entry.element.js';
|
|
11
12
|
function getFlattenedControlsFromHiddenParents(currentNodes, currentControls, currentNode, originalTree) {
|
|
12
13
|
const parent = traverseToImmediateParent(currentNode, originalTree);
|
|
13
14
|
const allControls = [];
|
|
@@ -81,7 +82,7 @@ export function createNodeTemplates({ currentNodes, isTopLevel, router, isSearch
|
|
|
81
82
|
}
|
|
82
83
|
else if (isBookTreeNode(currentNode, BookEntryType.ElementExample)) {
|
|
83
84
|
const controlsForElementExample = traverseControls(controls, currentNode.fullUrlBreadcrumbs.slice(0, -1));
|
|
84
|
-
|
|
85
|
+
const content = html `
|
|
85
86
|
<${BookElementExampleWrapper.assign({
|
|
86
87
|
elementExampleNode: currentNode,
|
|
87
88
|
currentPageControls: controlsForElementExample,
|
|
@@ -92,18 +93,28 @@ export function createNodeTemplates({ currentNodes, isTopLevel, router, isSearch
|
|
|
92
93
|
})}"
|
|
93
94
|
></${BookElementExampleWrapper}>
|
|
94
95
|
`;
|
|
96
|
+
return html `
|
|
97
|
+
<${BookLazyEntry.assign({
|
|
98
|
+
content,
|
|
99
|
+
})}></${BookLazyEntry}>
|
|
100
|
+
`;
|
|
95
101
|
}
|
|
96
102
|
else if (isBookTreeNode(currentNode, BookEntryType.Root)) {
|
|
97
103
|
return nothing;
|
|
98
104
|
}
|
|
99
105
|
else {
|
|
100
|
-
|
|
106
|
+
const content = html `
|
|
101
107
|
<${BookError.assign({
|
|
102
108
|
message: `Unknown entry type for rendering: '${currentNode.entry.entryType}'`,
|
|
103
109
|
})}
|
|
104
110
|
class="block-entry"
|
|
105
111
|
></${BookError}>
|
|
106
112
|
`;
|
|
113
|
+
return html `
|
|
114
|
+
<${BookLazyEntry.assign({
|
|
115
|
+
content,
|
|
116
|
+
})}></${BookLazyEntry}>
|
|
117
|
+
`;
|
|
107
118
|
}
|
|
108
119
|
});
|
|
109
120
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "element-book",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.13.0",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"book",
|
|
6
6
|
"design system",
|
|
@@ -41,28 +41,29 @@
|
|
|
41
41
|
"test:docs": "virmator docs check"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@augment-vir/assert": "^31.
|
|
45
|
-
"@augment-vir/common": "^31.
|
|
46
|
-
"@augment-vir/web": "^31.
|
|
44
|
+
"@augment-vir/assert": "^31.57.3",
|
|
45
|
+
"@augment-vir/common": "^31.57.3",
|
|
46
|
+
"@augment-vir/web": "^31.57.3",
|
|
47
47
|
"colorjs.io": "0.5.2",
|
|
48
|
+
"date-vir": "^8.1.0",
|
|
48
49
|
"lit-css-vars": "^3.0.11",
|
|
49
50
|
"spa-router-vir": "^6.4.1",
|
|
50
51
|
"typed-event-target": "^4.1.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@augment-vir/test": "^31.
|
|
54
|
+
"@augment-vir/test": "^31.57.3",
|
|
54
55
|
"@web/dev-server-esbuild": "^1.0.4",
|
|
55
56
|
"@web/test-runner": "^0.20.2",
|
|
56
57
|
"@web/test-runner-commands": "^0.9.0",
|
|
57
58
|
"@web/test-runner-playwright": "^0.11.1",
|
|
58
59
|
"@web/test-runner-visual-regression": "^0.10.0",
|
|
59
|
-
"element-vir": "^26.
|
|
60
|
+
"element-vir": "^26.13.0",
|
|
60
61
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
61
62
|
"markdown-code-example-inserter": "^3.0.3",
|
|
62
63
|
"type-fest": "^5.3.1",
|
|
63
64
|
"typedoc": "^0.28.15",
|
|
64
65
|
"typescript": "5.9.3",
|
|
65
|
-
"vira": "^28.
|
|
66
|
+
"vira": "^28.13.0"
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
69
|
"element-vir": ">=17",
|