Skip to content

Commit 6880215

Browse files
committed
Add static toggle
1 parent e0512c1 commit 6880215

File tree

5 files changed

+134
-97
lines changed

5 files changed

+134
-97
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<CH.StaticToggle />
2+
3+
<CH.Scrollycoding>
4+
5+
Hello
6+
7+
```js
8+
1
9+
```
10+
11+
</CH.Scrollycoding>

packages/mdx/src/components.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
import { Preview } from "./mdx-client/preview"
1616
import { InlineCode } from "./mdx-client/inline-code"
1717
import type { MDXComponents } from "mdx/types"
18+
import {
19+
useStaticToggle,
20+
StaticToggle,
21+
} from "./mdx-client/ssmq"
1822

1923
export {
2024
Code,
@@ -30,6 +34,8 @@ export {
3034
InlineCode,
3135
CodeSlot,
3236
PreviewSlot,
37+
useStaticToggle,
38+
StaticToggle,
3339
}
3440

3541
export const CH: MDXComponents = {
@@ -46,6 +52,7 @@ export const CH: MDXComponents = {
4652
InlineCode,
4753
CodeSlot,
4854
PreviewSlot,
55+
StaticToggle,
4956
}
5057

5158
import { MiniBrowser } from "./mini-browser"

packages/mdx/src/highlighter/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function highlight({
4444
)
4545
warnings.add(lang)
4646
}
47+
// potential endless loop
4748
return highlight({ code, lang: "text", theme })
4849
}
4950
}

packages/mdx/src/mdx-client/scrollycoding.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@ type ScrollycodingProps = {
2222
export function Scrollycoding(props) {
2323
return (
2424
<Swap
25-
match={[
26-
[
27-
props.codeConfig.staticMediaQuery,
28-
<StaticScrollycoding {...props} />,
29-
],
30-
["default", <DynamicScrollycoding {...props} />],
31-
]}
32-
/>
25+
query={props.codeConfig.staticMediaQuery}
26+
staticElement={<StaticScrollycoding {...props} />}
27+
>
28+
<DynamicScrollycoding {...props} />
29+
</Swap>
3330
)
3431
}
3532

packages/mdx/src/mdx-client/ssmq.tsx

Lines changed: 110 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,56 @@
33
import React from "react"
44

55
let suffixCounter = 0
6+
const PREFERS_STATIC_KEY = "ch-prefers-static"
7+
8+
export function toggleStatic() {
9+
localStorage.setItem(
10+
"ch-prefers-static",
11+
localStorage.getItem("ch-prefers-static") === "true"
12+
? "false"
13+
: "true"
14+
)
15+
window.dispatchEvent(
16+
new StorageEvent("storage", {
17+
key: "ch-prefers-static",
18+
})
19+
)
20+
}
21+
22+
export function StaticToggle({
23+
viewDynamicText = "View dynamic version",
24+
viewStaticText = "View static version",
25+
}) {
26+
const [forceStatic, toggleStatic] = useStaticToggle()
27+
return (
28+
<button
29+
onClick={toggleStatic}
30+
className="ch-static-toggle"
31+
data-ch-static={forceStatic}
32+
>
33+
{forceStatic ? viewDynamicText : viewStaticText}
34+
</button>
35+
)
36+
}
37+
38+
export function useStaticToggle() {
39+
const { showStatic: forceStatic } = useMedia(
40+
"screen and (max-width: 0px)"
41+
)
42+
43+
const [firstRender, setFirstRender] = React.useState(true)
44+
45+
React.useLayoutEffect(() => {
46+
if (forceStatic) {
47+
setFirstRender(false)
48+
}
49+
}, [])
50+
51+
return [
52+
firstRender ? false : forceStatic,
53+
toggleStatic,
54+
] as const
55+
}
656

757
/**
858
* @typedef SwapProps
@@ -14,139 +64,110 @@ let suffixCounter = 0
1464
* @param {SwapProps} props
1565
*/
1666

17-
export function Swap({ match }) {
18-
const queries = match.map(([q]) => q)
19-
const { isServer, matchedIndex } = useMedia(queries)
67+
export function Swap({ query, staticElement, children }) {
68+
const dynamicElement = children
69+
70+
const { isServer, showStatic } = useMedia(query)
2071
const mainClassName = isServer
2172
? "ssmq-" + suffixCounter++
2273
: ""
23-
2474
return isServer ? (
2575
<React.Fragment>
2676
<style
2777
className={mainClassName}
2878
dangerouslySetInnerHTML={{
29-
__html: getStyle(queries, mainClassName),
79+
__html: getStyle(query, mainClassName),
3080
}}
3181
/>
32-
{match.map(([query, element]) => (
33-
<div
34-
key={query}
35-
className={`${mainClassName} ${getClassName(
36-
query
37-
)}`}
38-
>
39-
{element}
40-
</div>
41-
))}
82+
<div className={`${mainClassName} ssmq-static`}>
83+
{staticElement}
84+
</div>
85+
<div className={`${mainClassName} ssmq-dynamic`}>
86+
{dynamicElement}
87+
</div>
4288
<script
4389
className={mainClassName}
4490
dangerouslySetInnerHTML={{
45-
__html: getScript(match, mainClassName),
91+
__html: getScript(query, mainClassName),
4692
}}
4793
/>
4894
</React.Fragment>
4995
) : (
5096
<React.Fragment>
51-
<div>{match[matchedIndex][1]}</div>
97+
<div>
98+
{showStatic ? staticElement : dynamicElement}
99+
</div>
52100
</React.Fragment>
53101
)
54102
}
55103

56-
function getStyle(queries, mainClass) {
57-
const reversedQueries = queries.slice().reverse()
58-
const style = reversedQueries
59-
.map(query => {
60-
const currentStyle = `.${mainClass}.${getClassName(
61-
query
62-
)}{display:block}`
63-
const otherStyle = `.${mainClass}:not(.${getClassName(
64-
query
65-
)}){display: none;}`
66-
67-
if (query === "default") {
68-
return `${currentStyle}${otherStyle}`
69-
} else {
70-
return `@media ${query}{${currentStyle}${otherStyle}}`
71-
}
72-
})
73-
.join("\n")
74-
return style
104+
function getStyle(query, mainClass) {
105+
return `.${mainClass}.ssmq-dynamic { display: block; }
106+
.${mainClass}.ssmq-static { display: none; }
107+
@media ${query} {
108+
.${mainClass}.ssmq-dynamic { display: none; }
109+
.${mainClass}.ssmq-static { display: block; }
110+
}
111+
`
75112
}
76113

77-
function getScript(match, mainClass) {
78-
const queries = match.map(([query]) => query)
79-
const classes = queries.map(getClassName)
114+
function getScript(query, mainClass) {
80115
return `(function() {
81-
var qs = ${JSON.stringify(queries)};
82-
var clss = ${JSON.stringify(classes)};
116+
var q = ${JSON.stringify(query)};
83117
var mainCls = "${mainClass}";
84118
85-
var scrEls = document.getElementsByTagName("script");
86-
var scrEl = scrEls[scrEls.length - 1];
87-
var parent = scrEl.parentNode;
119+
var dynamicEl = document.querySelector(
120+
"." + mainCls + ".ssmq-dynamic"
121+
)
122+
var staticEl = document.querySelector(
123+
"." + mainCls + ".ssmq-static"
124+
)
125+
var parent = dynamicEl.parentNode
88126
89-
var el = null;
90-
for (var i = 0; i < qs.length - 1; i++) {
91-
if (window.matchMedia(qs[i]).matches) {
92-
el = parent.querySelector(":scope > ." + mainCls + "." + clss[i]);
93-
break;
94-
}
127+
if (window.matchMedia(q).matches || localStorage.getItem("${PREFERS_STATIC_KEY}") === 'true') {
128+
staticEl.removeAttribute("class")
129+
} else {
130+
dynamicEl.removeAttribute("class")
95131
}
96-
if (!el) {
97-
var defaultClass = clss.pop();
98-
el = parent.querySelector(":scope > ." + mainCls + "." + defaultClass);
99-
}
100-
el.removeAttribute("class");
101132
102-
parent.querySelectorAll(":scope > ." + mainCls).forEach(function (e) {
103-
parent.removeChild(e);
104-
});
133+
parent
134+
.querySelectorAll(":scope > ." + mainCls)
135+
.forEach(function (e) {
136+
parent.removeChild(e)
137+
})
105138
})();`
106139
}
107140

108-
function getClassName(string) {
109-
return (
110-
"ssmq-" +
111-
string
112-
.replace(
113-
/[!\"#$%&'\(\)\*\+,\.\/:;<=>\?\@\[\\\]\^`\{\|\}~\s]/g,
114-
""
115-
)
116-
.toLowerCase()
117-
)
118-
}
119-
120-
function useMedia(queries) {
141+
function useMedia(query) {
121142
const isServer = typeof window === "undefined"
122143

123-
const allQueries = queries.slice(0, -1)
124-
125-
if (queries[queries.length - 1] !== "default") {
126-
console.warn("last media query should be 'default'")
144+
if (isServer) {
145+
return { isServer, showStatic: false }
127146
}
128147

129148
const [, setValue] = React.useState(0)
130149

131-
const mediaQueryLists = isServer
132-
? []
133-
: allQueries.map(q => window.matchMedia(q))
134-
150+
const mql = window.matchMedia(query)
135151
React.useEffect(() => {
136152
const handler = () => setValue(x => x + 1)
137-
mediaQueryLists.forEach(mql => mql.addListener(handler))
138-
return () =>
139-
mediaQueryLists.forEach(mql =>
140-
mql.removeListener(handler)
141-
)
153+
mql.addEventListener("change", handler)
154+
window.addEventListener("storage", event => {
155+
if (event.key === PREFERS_STATIC_KEY) {
156+
handler()
157+
}
158+
})
159+
return () => {
160+
mql.removeEventListener("change", handler)
161+
window.removeEventListener("storage", handler)
162+
}
142163
}, [])
143164

144-
const matchedIndex = mediaQueryLists.findIndex(
145-
mql => mql.matches
146-
)
165+
const showStatic =
166+
mql.matches ||
167+
localStorage.getItem(PREFERS_STATIC_KEY) === "true"
168+
147169
return {
148170
isServer,
149-
matchedIndex:
150-
matchedIndex < 0 ? queries.length - 1 : matchedIndex,
171+
showStatic,
151172
}
152173
}

0 commit comments

Comments
 (0)