Skip to content

Commit 72f3d46

Browse files
authored
Merge pull request #165 from takker99:switch-pages
feat: Use the tab UI when there are the same title's pages in some projects
2 parents 131474f + b63cf6d commit 72f3d46

File tree

7 files changed

+242
-129
lines changed

7 files changed

+242
-129
lines changed

Bubble.tsx

Lines changed: 16 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,24 @@
33
/// <reference no-default-lib="true"/>
44
/// <reference lib="esnext"/>
55
/// <reference lib="dom"/>
6-
import { Page } from "./Page.tsx";
76
import { CardList } from "./CardList.tsx";
87
import {
98
Fragment,
10-
FunctionComponent,
119
h,
12-
useCallback,
1310
useLayoutEffect,
1411
useMemo,
1512
useState,
1613
} from "./deps/preact.tsx";
17-
import { encodeTitleURI, toTitleLc } from "./deps/scrapbox-std.ts";
14+
import { toTitleLc } from "./deps/scrapbox-std.ts";
1815
import { useBubbleData } from "./useBubbleData.ts";
19-
import { useTheme } from "./useTheme.ts";
2016
import { LinkTo } from "./types.ts";
2117
import { fromId, ID, toId } from "./id.ts";
2218
import { Bubble as BubbleData } from "./storage.ts";
23-
import { original, produce } from "./deps/immer.ts";
19+
import { produce } from "./deps/immer.ts";
2420
import type { BubbleSource } from "./useBubbles.ts";
2521
import { createDebug } from "./debug.ts";
2622
import type { Scrapbox } from "./deps/scrapbox.ts";
23+
import { TextBubble } from "./TextBubble.tsx";
2724
declare const scrapbox: Scrapbox;
2825

2926
const logger = createDebug("ScrapBubble:Bubble.tsx");
@@ -57,54 +54,21 @@ export const Bubble = ({
5754
parentTitles,
5855
);
5956

60-
const handleClick = useCallback(() => props.hide(), [props.hide]);
61-
const theme = useTheme(pages[0]?.project ?? source.project);
62-
const pageStyle = useMemo(() => ({
63-
top: `${source.position.top}px`,
64-
maxWidth: `${source.position.maxWidth}px`,
65-
...("left" in source.position
66-
? {
67-
left: `${source.position.left}px`,
68-
}
69-
: {
70-
right: `${source.position.right}px`,
71-
}),
72-
}), [
73-
source.position,
74-
]);
75-
7657
return (
7758
<>
78-
{pages.length > 0 && (
79-
<div
80-
className="text-bubble"
81-
style={pageStyle}
82-
data-theme={theme}
83-
onClick={handleClick}
84-
>
85-
<StatusBar>
86-
{pages[0].project !== scrapbox.Project.name && (
87-
<ProjectBadge
88-
project={pages[0].project}
89-
title={pages[0].lines[0].text}
90-
/>
91-
)}
92-
</StatusBar>
93-
<Page
94-
lines={pages[0].lines}
95-
project={pages[0].project}
96-
title={pages[0].lines[0].text}
97-
hash={source.hash}
98-
linkTo={source.linkTo}
99-
whiteList={whiteList}
100-
{...props}
101-
/>
102-
</div>
59+
{hasOneBubble(pages) && (
60+
<TextBubble
61+
pages={pages}
62+
source={source}
63+
whiteList={whiteList}
64+
onClick={props.hide}
65+
{...props}
66+
/>
10367
)}
10468
<CardList
10569
linked={linked}
10670
externalLinked={externalLinked}
107-
onClick={handleClick}
71+
onClick={props.hide}
10872
source={source}
10973
projectsForSort={projects}
11074
{...props}
@@ -113,6 +77,10 @@ export const Bubble = ({
11377
);
11478
};
11579

80+
const hasOneBubble = (
81+
pages: BubbleData[],
82+
): pages is [BubbleData, ...BubbleData[]] => pages.length > 0;
83+
11684
/** 指定したsourceからbubblesするページ本文とページカードを、親bubblesやwhiteListを使って絞り込む
11785
*
11886
* <Bubble />でやっている処理の一部を切り出して見通しをよくしただけ
@@ -222,21 +190,3 @@ const useBubbleFilter = (
222190

223191
return [linked, externalLinked, pages] as const;
224192
};
225-
226-
const StatusBar: FunctionComponent = ({ children }) => (
227-
<div className="status-bar top-right">{children}</div>
228-
);
229-
230-
type ProjectBadgeProps = {
231-
project: string;
232-
title: string;
233-
};
234-
const ProjectBadge = ({ project, title }: ProjectBadgeProps): h.JSX.Element => (
235-
<a
236-
href={`/${project}/${encodeTitleURI(title)}`}
237-
target="_blank"
238-
rel="noopener noreferrer"
239-
>
240-
{project}
241-
</a>
242-
);

Page.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
YoutubeNode,
5656
} from "./deps/scrapbox-std.ts";
5757
import type { Scrapbox } from "./deps/scrapbox.ts";
58+
import { useTheme } from "./useTheme.ts";
5859
declare const scrapbox: Scrapbox;
5960

6061
declare global {
@@ -162,8 +163,10 @@ export const Page = (
162163
globalThis.scroll(0, scrollY);
163164
}, [scrollId]);
164165

166+
const theme = useTheme(project);
167+
165168
return (
166-
<div className="lines" ref={ref}>
169+
<div className="lines" data-theme={theme} ref={ref}>
167170
<context.Provider
168171
value={{ project, title, whiteList, ...props }}
169172
>
@@ -179,7 +182,17 @@ export const Page = (
179182
noIndent={noIndent}
180183
permalink={block.id === scrollId}
181184
>
182-
{block.text}
185+
<a
186+
className="page-link"
187+
type="link"
188+
href={`/${project}/${encodeTitleURI(block.text)}`}
189+
rel={project === scrapbox.Project.name
190+
? "route"
191+
: "noopener noreferrer"}
192+
target={project === scrapbox.Project.name ? "" : "_blank"}
193+
>
194+
{block.text}
195+
</a>
183196
</Line>
184197
<hr />
185198
</>

TextBubble.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/// <reference no-default-lib="true"/>
2+
/// <reference lib="esnext"/>
3+
/// <reference lib="dom"/>
4+
5+
/** @jsx h */
6+
import {
7+
FunctionComponent,
8+
h,
9+
useCallback,
10+
useMemo,
11+
useState,
12+
} from "./deps/preact.tsx";
13+
import { ID, toId } from "./id.ts";
14+
import { Page, PageProps } from "./Page.tsx";
15+
import { Bubble } from "./storage.ts";
16+
import { BubbleOperators, Source } from "./useBubbles.ts";
17+
import { useTheme } from "./useTheme.ts";
18+
19+
export interface TextBubble extends BubbleOperators {
20+
pages: [Bubble, ...Bubble[]];
21+
whiteList: Set<string>;
22+
delay: number;
23+
noIndent?: boolean;
24+
source: Source;
25+
prefetch: (project: string, title: string) => void;
26+
onClick?: h.JSX.MouseEventHandler<HTMLDivElement>;
27+
}
28+
export const TextBubble: FunctionComponent<TextBubble> = (
29+
{ pages, onClick, source, whiteList, ...rest },
30+
) => {
31+
const [activeTab, setActiveTab] = useState(
32+
toId(pages[0].project, pages[0].titleLc),
33+
);
34+
35+
const pageStyle = useMemo(() => ({
36+
top: `${source.position.top}px`,
37+
maxWidth: `${source.position.maxWidth}px`,
38+
...("left" in source.position
39+
? {
40+
left: `${source.position.left}px`,
41+
}
42+
: {
43+
right: `${source.position.right}px`,
44+
}),
45+
}), [source.position]);
46+
return (
47+
<div
48+
className="text-bubble"
49+
style={pageStyle}
50+
onClick={onClick}
51+
>
52+
{pages.length > 1 &&
53+
(
54+
<div role="tablist">
55+
{pages.map((page) => (
56+
<Tab
57+
key={toId(page.project, page.titleLc)}
58+
project={page.project}
59+
titleLc={page.titleLc}
60+
selected={activeTab ===
61+
toId(page.project, page.titleLc)}
62+
tabSelector={setActiveTab}
63+
/>
64+
))}
65+
</div>
66+
)}
67+
{pages.map((page) => (
68+
<TabPanel
69+
key={toId(page.project, page.titleLc)}
70+
selected={activeTab ===
71+
toId(page.project, page.titleLc)}
72+
{...page}
73+
title={page.lines[0].text}
74+
hash={source.hash}
75+
linkTo={source.linkTo}
76+
whiteList={whiteList}
77+
{...rest}
78+
/>
79+
))}
80+
</div>
81+
);
82+
};
83+
84+
const Tab: FunctionComponent<
85+
{
86+
project: string;
87+
titleLc: string;
88+
selected: boolean;
89+
tabSelector: (id: ID) => void;
90+
}
91+
> = ({ project, titleLc, tabSelector, selected }) => {
92+
const handleClick = useCallback(() => tabSelector(toId(project, titleLc)), [
93+
project,
94+
titleLc,
95+
]);
96+
const theme = useTheme(project);
97+
return (
98+
<button
99+
role="tab"
100+
aria-selected={selected}
101+
data-theme={theme}
102+
tabIndex={-1}
103+
onClick={handleClick}
104+
>
105+
{project}
106+
</button>
107+
);
108+
};
109+
110+
const TabPanel: FunctionComponent<{ selected: boolean } & PageProps> = (
111+
{ selected, ...rest },
112+
) => {
113+
const theme = useTheme(rest.project);
114+
return (
115+
<div
116+
role="tabpanel"
117+
data-theme={theme}
118+
hidden={!selected}
119+
>
120+
<Page {...rest} />
121+
</div>
122+
);
123+
};

app.min.css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

deps/esbuild.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
//@deno-types=https://cdn.jsdelivr.net/npm/esbuild-wasm@0.14.10/esm/browser.d.ts
2-
export * from "https://cdn.jsdelivr.net/npm/esbuild-wasm@0.14.10/esm/browser.js";
1+
//@deno-types=https://cdn.jsdelivr.net/npm/esbuild-wasm@0.21.5/esm/browser.d.ts
2+
export * from "https://cdn.jsdelivr.net/npm/esbuild-wasm@0.21.5/esm/browser.js";

scripts/minifyCSS.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,28 @@ import { build, initialize } from "../deps/esbuild.ts";
22

33
// prepare esbuild
44
await initialize({
5-
wasmURL: "https://cdn.jsdelivr.net/npm/esbuild-wasm@0.14.10/esbuild.wasm",
5+
wasmURL: "https://cdn.jsdelivr.net/npm/esbuild-wasm@0.21.5/esbuild.wasm",
66
worker: false,
77
});
88

99
// bundle & minify app.css
1010
const name = "file-loader";
1111
const { outputFiles: [css] } = await build({
12-
stdin: {
13-
loader: "css",
14-
contents: '@import "../app.css";',
15-
},
12+
entryPoints: [new URL("../app.css", import.meta.url).href],
1613
bundle: true,
1714
minify: true,
1815
write: false,
1916
plugins: [{
2017
name,
2118
setup: ({ onLoad, onResolve }) => {
2219
onResolve({ filter: /.*/ }, ({ path, importer }) => {
23-
importer = importer === "<stdin>" ? import.meta.url : importer;
2420
return {
25-
path: new URL(path, importer).href,
21+
path: importer ? new URL(path, importer).href : path,
2622
namespace: name,
2723
};
2824
});
2925
onLoad({ filter: /.*/, namespace: name }, async ({ path }) => ({
30-
contents: await Deno.readTextFile(new URL(path)),
26+
contents: await (await fetch(new URL(path))).text(),
3127
loader: "css",
3228
}));
3329
},

0 commit comments

Comments
 (0)