Skip to content

Commit cce8653

Browse files
pagetabs: persistent selection (#1512)
1 parent 721e843 commit cce8653

6 files changed

Lines changed: 140 additions & 62 deletions

File tree

znai-docs/znai/llm.txt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ answer-link: znai-from-export/introduction/getting-started#command-line
146146
## CLI download
147147

148148
Download and unzip
149-
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.88/znai-dist-1.88-znai.zip). Add
149+
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.89/znai-dist-1.89-znai.zip). Add
150150
it to your `PATH`.
151151

152152
## Brew
@@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin
162162
<plugin>
163163
<groupId>org.testingisdocumenting.znai</groupId>
164164
<artifactId>znai-maven-plugin</artifactId>
165-
<version>1.88</version>
165+
<version>1.89</version>
166166
</plugin>
167167
```
168168

@@ -897,10 +897,10 @@ To define a footnote use
897897

898898
```
899899
[^my-id]: extra content for my footnote goes here
900-
potentially including code blocks
901-
```
902-
Constructor()
903-
```
900+
potentially including code blocks
901+
```
902+
Constructor()
903+
```
904904
```
905905

906906
To add a reference to the footnote use `[^my-id]` which will result in [^my-id]
@@ -913,7 +913,19 @@ appearance.
913913
# Flow :: Footnotes :: Preview
914914
answer-link: znai-from-export/flow/footnotes#preview
915915

916-
Hover mouse over a footnote reference see its content in a tooltip.
916+
Hover mouse over a footnote reference see its content in a tooltip. Click on it to go the footnotes list at the bottom
917+
of the page.
918+
919+
# Flow :: Footnotes :: Footnotes Inside Attention Blocks
920+
answer-link: znai-from-export/flow/footnotes#footnotes-inside-attention-blocks
921+
922+
Footnotes can be used inside different content blocks like `attention-note`:
923+
924+
```
925+
```attention-warning
926+
Use something important [^my-id]
927+
```
928+
```
917929

918930
# Flow :: Footnotes :: Footnotes List
919931
answer-link: znai-from-export/flow/footnotes#footnotes-list
@@ -9356,6 +9368,11 @@ export PATH=$(pwd)/dist:$PATH
93569368
znai --version
93579369
```
93589370

9371+
# Release Notes :: 2026 :: 1.89
9372+
answer-link: znai-from-export/release-notes/2026#189
9373+
9374+
9375+
93599376
# Release Notes :: 2026 :: 1.88
93609377
answer-link: znai-from-export/release-notes/2026#188
93619378

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add: [Page Tabs](layout/page-tabs) selection is maintained across the pages and also survives the page reload

znai-docs/znai/release-notes/2026.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.90
2+
3+
:include-markdowns: 1.90
4+
15
# 1.89
26

37
:include-markdowns: 1.89

znai-reactjs/src/doc-elements/page/page-tabs/PageTabsPageContent.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { afterTitleId } from "../../../layout/classNamesAndIds";
1919
import PageTabsSelection from "./PageTabsSelection";
2020
import { buildContentForTab } from "./pageTabsContentUtils";
2121
import { findParentWithScroll } from "../../../utils/domNodes";
22+
import { tabsRegistration, TabSwitchEvent } from "../../tabs/TabsRegistration";
2223

2324
interface ScrollSnapshot {
2425
parentWithScroll: HTMLElement;
@@ -36,10 +37,19 @@ class PageTabsPageContent extends React.Component<any, PageTabsState> {
3637
constructor(props: any) {
3738
super(props);
3839

39-
this.state = { activeTabId: "" };
40+
const { tabIds } = props;
41+
this.state = { activeTabId: tabsRegistration.firstMatchFromHistory(tabIds) || "" };
4042
this.contentRef = React.createRef();
4143
}
4244

45+
componentDidMount() {
46+
tabsRegistration.addTabSwitchListener(this.onTabSwitch);
47+
}
48+
49+
componentWillUnmount() {
50+
tabsRegistration.removeTabSwitchListener(this.onTabSwitch);
51+
}
52+
4353
render() {
4454
const { elementsLibrary, content, tabIds, ...props } = this.props;
4555
const { PageTitle } = elementsLibrary;
@@ -69,7 +79,14 @@ class PageTabsPageContent extends React.Component<any, PageTabsState> {
6979
return;
7080
}
7181

72-
this.setState({ activeTabId: tabId });
82+
tabsRegistration.notifyNewTab({ tabName: tabId, triggeredNode: this.contentRef.current });
83+
};
84+
85+
onTabSwitch = ({ tabName }: TabSwitchEvent) => {
86+
const { tabIds } = this.props;
87+
if (tabIds.includes(tabName) && tabName !== this.state.activeTabId) {
88+
this.setState({ activeTabId: tabName });
89+
}
7390
};
7491

7592
getSnapshotBeforeUpdate(_prevProps: any, prevState: PageTabsState): ScrollSnapshot | null {

znai-reactjs/src/doc-elements/tabs/TabsRegistration.js

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
* Copyright 2019 TWO SIGMA OPEN SOURCE, LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
interface TabSwitchEvent {
19+
tabName: string;
20+
triggeredNode: HTMLElement | null;
21+
}
22+
23+
type TabSwitchListener = (event: TabSwitchEvent) => void;
24+
25+
const STORAGE_KEY = "znai-tabs-selection-history";
26+
const MAX_HISTORY = 50;
27+
28+
class TabsRegistration {
29+
private listeners: TabSwitchListener[] = [];
30+
private tabsSelectionHistory: string[] = loadHistory();
31+
32+
addTabSwitchListener(listener: TabSwitchListener) {
33+
this.listeners.push(listener);
34+
}
35+
36+
removeTabSwitchListener(listener: TabSwitchListener) {
37+
removeFromArray(this.listeners, listener);
38+
}
39+
40+
firstMatchFromHistory(names: string[]): string {
41+
return this.tabsSelectionHistory.find((n) => names.indexOf(n) >= 0) ?? names[0];
42+
}
43+
44+
notifyNewTab({ tabName, triggeredNode }: TabSwitchEvent) {
45+
removeFromArray(this.tabsSelectionHistory, tabName);
46+
this.tabsSelectionHistory.unshift(tabName);
47+
48+
if (this.tabsSelectionHistory.length > MAX_HISTORY) {
49+
this.tabsSelectionHistory.length = MAX_HISTORY;
50+
}
51+
52+
saveHistory(this.tabsSelectionHistory);
53+
54+
this.listeners.forEach((l) => l({ tabName, triggeredNode }));
55+
}
56+
}
57+
58+
function loadHistory(): string[] {
59+
try {
60+
const stored = localStorage.getItem(STORAGE_KEY);
61+
if (stored) {
62+
const parsed = JSON.parse(stored);
63+
if (Array.isArray(parsed)) {
64+
return parsed.filter((item: unknown) => typeof item === "string");
65+
}
66+
}
67+
} catch (e) {
68+
console.warn("failed to load tabs selection history", e);
69+
}
70+
71+
return [];
72+
}
73+
74+
function saveHistory(history: string[]) {
75+
try {
76+
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
77+
} catch (e) {
78+
console.warn("failed to save tabs selection history", e);
79+
}
80+
}
81+
82+
function removeFromArray<T>(array: T[], value: T) {
83+
const idx = array.indexOf(value);
84+
if (idx !== -1) {
85+
array.splice(idx, 1);
86+
}
87+
}
88+
89+
const tabsRegistration = new TabsRegistration();
90+
91+
export { tabsRegistration };
92+
export type { TabSwitchEvent };

0 commit comments

Comments
 (0)