@m3e/toc 1.0.0-rc.1

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.
@@ -0,0 +1,74 @@
1
+ import { getTextContent, guid } from "@m3e/core";
2
+
3
+ /** A node in a table of contents. */
4
+ export interface TocNode {
5
+ /** An opaque identifier that uniquely identifies the node. */
6
+ id: string;
7
+
8
+ /** The level of the node. */
9
+ level: number;
10
+
11
+ /** The text to display for the node. */
12
+ label: string;
13
+
14
+ /** The element of the node. */
15
+ element: HTMLElement;
16
+
17
+ /** The child nodes. */
18
+ nodes: TocNode[];
19
+ }
20
+
21
+ /** Provides functionality used to generate a table of contents used for in-page navigation. */
22
+ export class TocGenerator {
23
+ /**
24
+ * Generates nodes from which to construct a table of contents for in-page navigation.
25
+ * @param {HTMLElement} element The element for which to generate a table of contents.
26
+ * @param {number} [maxDepth=6] The maximum depth of the table of contents.
27
+ * @returns {Array<TocNode>} The top-level nodes of the table of contents.
28
+ */
29
+ static generate(element: HTMLElement, maxDepth: number = 6): Array<TocNode> {
30
+ const maxLevel = 6;
31
+ let topLevel = maxLevel;
32
+ const nodes = new Array<TocNode>();
33
+ element
34
+ .querySelectorAll<HTMLElement>(
35
+ "h1:not([m3e-toc-ignore]),h2:not([m3e-toc-ignore]),h3:not([m3e-toc-ignore]),h4:not([m3e-toc-ignore]),h5:not([m3e-toc-ignore]),h6:not([m3e-toc-ignore]),m3e-heading[level]:not([m3e-toc-ignore])"
36
+ )
37
+ .forEach((element) => {
38
+ const level = TocGenerator.#getHeaderLevel(element);
39
+ topLevel = Math.min(level, topLevel);
40
+ nodes.push({
41
+ id: element.id || guid(),
42
+ element,
43
+ level,
44
+ label: getTextContent(element, true),
45
+ nodes: new Array<TocNode>(),
46
+ });
47
+ });
48
+
49
+ for (let level = topLevel + maxDepth - 1; level > topLevel; level--) {
50
+ for (let i = 0; i < nodes.length; i++) {
51
+ const node = nodes[i];
52
+ if (node.level === level) {
53
+ for (let j = i; j >= 0; j--) {
54
+ const prev = nodes[j];
55
+ if (prev.level < level) {
56
+ prev.nodes.push(node);
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ nodes.forEach((x) => (x.level -= topLevel - 1));
65
+ return nodes.filter((x) => x.level === 1);
66
+ }
67
+
68
+ /** @internal */
69
+ static #getHeaderLevel(element: HTMLElement): number {
70
+ return element.tagName.startsWith("H")
71
+ ? parseInt(element.tagName.substring(1))
72
+ : parseInt(element.getAttribute("level") ?? "0");
73
+ }
74
+ }
@@ -0,0 +1,121 @@
1
+ import { css, CSSResultGroup, html, LitElement, PropertyValues, unsafeCSS } from "lit";
2
+ import { customElement, query, state } from "lit/decorators.js";
3
+
4
+ import { AttachInternals, DesignToken, Disabled, M3eStateLayerElement, Role, Selected } from "@m3e/core";
5
+
6
+ import { TocNode } from "./TocGenerator";
7
+
8
+ /**
9
+ * An item in a table of contents.
10
+ * @tag m3e-toc-item
11
+ *
12
+ * @slot - Renders the label of the item.
13
+ *
14
+ * @attr disabled - A value indicating whether the element is disabled.
15
+ *
16
+ * @cssprop --m3e-toc-item-shape - Border radius of the TOC item.
17
+ * @cssprop --m3e-toc-item-padding-block - Block padding for the TOC item.
18
+ * @cssprop --m3e-toc-item-padding - Inline padding for the TOC item.
19
+ * @cssprop --m3e-toc-item-inset - Indentation per level for the TOC item.
20
+ * @cssprop --m3e-toc-active-indicator-animation-duration - Animation duration for the active indicator.
21
+ * @cssprop --m3e-toc-item-font-size - Font size for unselected items.
22
+ * @cssprop --m3e-toc-item-font-weight - Font weight for unselected items.
23
+ * @cssprop --m3e-toc-item-line-height - Line height for unselected items.
24
+ * @cssprop --m3e-toc-item-tracking - Letter spacing for unselected items.
25
+ * @cssprop --m3e-toc-item-color - Text color for unselected items.
26
+ * @cssprop --m3e-toc-item-selected-font-size - Font size for selected items.
27
+ * @cssprop --m3e-toc-item-selected-font-weight - Font weight for selected items.
28
+ * @cssprop --m3e-toc-item-selected-line-height - Line height for selected items.
29
+ * @cssprop --m3e-toc-item-selected-tracking - Letter spacing for selected items.
30
+ * @cssprop --m3e-toc-item-selected-color - Text color for selected items.
31
+ */
32
+ @customElement("m3e-toc-item")
33
+ export class M3eTocItemElement extends Selected(Disabled(AttachInternals(Role(LitElement, "link")))) {
34
+ /** The styles of the element. */
35
+ static override styles: CSSResultGroup = css`
36
+ :host {
37
+ display: inline-block;
38
+ position: relative;
39
+ user-select: none;
40
+ border-radius: var(--m3e-toc-item-shape, ${DesignToken.shape.corner.largeIncreased});
41
+ padding-block: var(--m3e-toc-item-padding-block, 0.5rem);
42
+ }
43
+ :host(:not(:disabled)) {
44
+ cursor: pointer;
45
+ }
46
+ .base {
47
+ padding-inline-start: calc(
48
+ var(--m3e-toc-item-padding, 1rem) + calc(var(--m3e-toc-item-inset, 0.75rem) * var(--_level, 0))
49
+ );
50
+ padding-inline-end: var(--m3e-toc-item-padding, 1rem);
51
+ transition: ${unsafeCSS(
52
+ `color var(--m3e-toc-active-indicator-animation-duration, ${DesignToken.motion.duration.long1}) ${DesignToken.motion.easing.standard}`
53
+ )};
54
+ }
55
+ :host(:not([selected])) {
56
+ font-size: var(--m3e-toc-item-font-size, ${DesignToken.typescale.standard.body.large.fontSize});
57
+ font-weight: var(--m3e-toc-item-font-weight, ${DesignToken.typescale.standard.body.large.fontWeight});
58
+ line-height: var(--m3e-toc-item-line-height, ${DesignToken.typescale.standard.body.large.lineHeight});
59
+ letter-spacing: var(--m3e-toc-item-tracking, ${DesignToken.typescale.standard.body.large.tracking});
60
+ color: var(--m3e-toc-item-color, ${DesignToken.color.onSurfaceVariant});
61
+ }
62
+ :host([selected]) {
63
+ font-size: var(--m3e-toc-item-selected-font-size, ${DesignToken.typescale.emphasized.body.large.fontSize});
64
+ font-weight: var(--m3e-toc-item-selected-font-weight, ${DesignToken.typescale.emphasized.body.large.fontWeight});
65
+ line-height: var(--m3e-toc-item-selected-line-height, ${DesignToken.typescale.emphasized.body.large.lineHeight});
66
+ letter-spacing: var(--m3e-toc-item-selected-tracking, ${DesignToken.typescale.emphasized.body.large.tracking});
67
+ color: var(--m3e-toc-item-selected-color, ${DesignToken.color.onSecondaryContainer});
68
+ }
69
+ .base {
70
+ justify-content: unset;
71
+ }
72
+ .state-layer {
73
+ --m3e-state-layer-focus-opacity: 0;
74
+ }
75
+ @media (prefers-reduced-motion) {
76
+ .base {
77
+ transition: none;
78
+ }
79
+ }
80
+ `;
81
+
82
+ /** @private */ @query(".base") private readonly _base?: HTMLElement;
83
+ /** @private */ @query(".state-layer") private readonly _stateLayer?: M3eStateLayerElement;
84
+ /** @internal */ @state() node?: TocNode;
85
+
86
+ /** @internal */
87
+ protected override update(changedProperties: PropertyValues<this>): void {
88
+ super.update(changedProperties);
89
+
90
+ if (changedProperties.has("selected")) {
91
+ this.ariaSelected = null;
92
+ this.ariaCurrent = this.selected ? "true" : null;
93
+ }
94
+
95
+ if (changedProperties.has("node")) {
96
+ if (this.node) {
97
+ this._base?.style.setProperty("--_level", `${this.node.level - 1}`);
98
+ } else {
99
+ this._base?.style.removeProperty("--_level");
100
+ }
101
+ }
102
+ }
103
+
104
+ /** @inheritdoc */
105
+ protected override firstUpdated(_changedProperties: PropertyValues<this>): void {
106
+ super.firstUpdated(_changedProperties);
107
+ this._stateLayer?.attach(this);
108
+ }
109
+
110
+ /** @inheritdoc */
111
+ override render(): unknown {
112
+ return html`<m3e-state-layer class="state-layer"></m3e-state-layer>
113
+ <div class="base"><slot></slot></div>`;
114
+ }
115
+ }
116
+
117
+ declare global {
118
+ interface HTMLElementTagNameMap {
119
+ "m3e-toc-item": M3eTocItemElement;
120
+ }
121
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./TocElement";
2
+ export * from "./TocGenerator";
3
+ export * from "./TocItemElement";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist/src"
6
+ },
7
+ "include": ["src/**/*.ts", "**/*.mjs", "**/*.js"],
8
+ "exclude": []
9
+ }