Skip to content

Commit 111f44a

Browse files
authored
Merge pull request #395 from code-hike/static-toggle
Add static toggle
2 parents e0512c1 + 984614e commit 111f44a

File tree

6 files changed

+141
-109
lines changed

6 files changed

+141
-109
lines changed

packages/mdx/dev/content/scrollycoding-demo.mdx

+6-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
44

55
Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies.
66

7-
<CH.Scrollycoding>
7+
<CH.Scrollycoding showCopyButton={false} rows="focus" showExpandButton={true}>
88

99
## Step 1
1010

@@ -18,7 +18,7 @@ Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdie
1818

1919
<CH.Code>
2020

21-
```js app.js focus=3:10
21+
```js focus=3:10
2222
const { lorem, ipsum } = dolor({
2323
sit: {
2424
amet: 1,
@@ -55,12 +55,6 @@ const { lorem, ipsum } = dolor({
5555
})
5656
```
5757

58-
```css styles.css
59-
.lorem {
60-
color: red;
61-
}
62-
```
63-
6458
</CH.Code>
6559

6660
---
@@ -75,7 +69,7 @@ Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volu
7569

7670
Morbi quis commodo.
7771

78-
```js app.js focus=11:17
72+
```js focus=11:17
7973

8074
```
8175

@@ -91,7 +85,7 @@ Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volu
9185

9286
Ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.
9387

94-
```js app.js focus=11:14
88+
```js focus=11:14
9589
const { lorem, ipsum } = dolor({
9690
sit: {
9791
amet: 1,
@@ -138,7 +132,7 @@ Sed blandit libero volutpat sed cras.
138132

139133
Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae.
140134

141-
```js app.js focus=19:29
135+
```js focus=19:29
142136

143137
```
144138

@@ -159,7 +153,7 @@ Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed vi
159153

160154
Morbi quis commodo.
161155

162-
```js app.js
156+
```js
163157

164158
```
165159

+11
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

+7
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

+1
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

+6-8
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

@@ -213,6 +210,7 @@ function DynamicScrollycoding({
213210
{...rest}
214211
{...(tab as any)}
215212
codeConfig={codeConfig}
213+
rows={undefined}
216214
onTabClick={onTabClick}
217215
/>
218216
{presetConfig ? (

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

+110-89
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)