Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static toggle #395

Merged
merged 2 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions packages/mdx/dev/content/scrollycoding-demo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i

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.

<CH.Scrollycoding>
<CH.Scrollycoding showCopyButton={false} rows="focus" showExpandButton={true}>

## Step 1

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

<CH.Code>

```js app.js focus=3:10
```js focus=3:10
const { lorem, ipsum } = dolor({
sit: {
amet: 1,
Expand Down Expand Up @@ -55,12 +55,6 @@ const { lorem, ipsum } = dolor({
})
```

```css styles.css
.lorem {
color: red;
}
```

</CH.Code>

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

Morbi quis commodo.

```js app.js focus=11:17
```js focus=11:17

```

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

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

```js app.js focus=11:14
```js focus=11:14
const { lorem, ipsum } = dolor({
sit: {
amet: 1,
Expand Down Expand Up @@ -138,7 +132,7 @@ Sed blandit libero volutpat sed cras.

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

```js app.js focus=19:29
```js focus=19:29

```

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

Morbi quis commodo.

```js app.js
```js

```

Expand Down
11 changes: 11 additions & 0 deletions packages/mdx/dev/content/static-toggle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<CH.StaticToggle />

<CH.Scrollycoding>

Hello

```js
1
```

</CH.Scrollycoding>
7 changes: 7 additions & 0 deletions packages/mdx/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
import { Preview } from "./mdx-client/preview"
import { InlineCode } from "./mdx-client/inline-code"
import type { MDXComponents } from "mdx/types"
import {
useStaticToggle,
StaticToggle,
} from "./mdx-client/ssmq"

export {
Code,
Expand All @@ -30,6 +34,8 @@ export {
InlineCode,
CodeSlot,
PreviewSlot,
useStaticToggle,
StaticToggle,
}

export const CH: MDXComponents = {
Expand All @@ -46,6 +52,7 @@ export const CH: MDXComponents = {
InlineCode,
CodeSlot,
PreviewSlot,
StaticToggle,
}

import { MiniBrowser } from "./mini-browser"
Expand Down
1 change: 1 addition & 0 deletions packages/mdx/src/highlighter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function highlight({
)
warnings.add(lang)
}
// potential endless loop
return highlight({ code, lang: "text", theme })
}
}
14 changes: 6 additions & 8 deletions packages/mdx/src/mdx-client/scrollycoding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ type ScrollycodingProps = {
export function Scrollycoding(props) {
return (
<Swap
match={[
[
props.codeConfig.staticMediaQuery,
<StaticScrollycoding {...props} />,
],
["default", <DynamicScrollycoding {...props} />],
]}
/>
query={props.codeConfig.staticMediaQuery}
staticElement={<StaticScrollycoding {...props} />}
>
<DynamicScrollycoding {...props} />
</Swap>
)
}

Expand Down Expand Up @@ -213,6 +210,7 @@ function DynamicScrollycoding({
{...rest}
{...(tab as any)}
codeConfig={codeConfig}
rows={undefined}
onTabClick={onTabClick}
/>
{presetConfig ? (
Expand Down
199 changes: 110 additions & 89 deletions packages/mdx/src/mdx-client/ssmq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@
import React from "react"

let suffixCounter = 0
const PREFERS_STATIC_KEY = "ch-prefers-static"

export function toggleStatic() {
localStorage.setItem(
"ch-prefers-static",
localStorage.getItem("ch-prefers-static") === "true"
? "false"
: "true"
)
window.dispatchEvent(
new StorageEvent("storage", {
key: "ch-prefers-static",
})
)
}

export function StaticToggle({
viewDynamicText = "View dynamic version",
viewStaticText = "View static version",
}) {
const [forceStatic, toggleStatic] = useStaticToggle()
return (
<button
onClick={toggleStatic}
className="ch-static-toggle"
data-ch-static={forceStatic}
>
{forceStatic ? viewDynamicText : viewStaticText}
</button>
)
}

export function useStaticToggle() {
const { showStatic: forceStatic } = useMedia(
"screen and (max-width: 0px)"
)

const [firstRender, setFirstRender] = React.useState(true)

React.useLayoutEffect(() => {
if (forceStatic) {
setFirstRender(false)
}
}, [])

return [
firstRender ? false : forceStatic,
toggleStatic,
] as const
}

/**
* @typedef SwapProps
Expand All @@ -14,139 +64,110 @@ let suffixCounter = 0
* @param {SwapProps} props
*/

export function Swap({ match }) {
const queries = match.map(([q]) => q)
const { isServer, matchedIndex } = useMedia(queries)
export function Swap({ query, staticElement, children }) {
const dynamicElement = children

const { isServer, showStatic } = useMedia(query)
const mainClassName = isServer
? "ssmq-" + suffixCounter++
: ""

return isServer ? (
<React.Fragment>
<style
className={mainClassName}
dangerouslySetInnerHTML={{
__html: getStyle(queries, mainClassName),
__html: getStyle(query, mainClassName),
}}
/>
{match.map(([query, element]) => (
<div
key={query}
className={`${mainClassName} ${getClassName(
query
)}`}
>
{element}
</div>
))}
<div className={`${mainClassName} ssmq-static`}>
{staticElement}
</div>
<div className={`${mainClassName} ssmq-dynamic`}>
{dynamicElement}
</div>
<script
className={mainClassName}
dangerouslySetInnerHTML={{
__html: getScript(match, mainClassName),
__html: getScript(query, mainClassName),
}}
/>
</React.Fragment>
) : (
<React.Fragment>
<div>{match[matchedIndex][1]}</div>
<div>
{showStatic ? staticElement : dynamicElement}
</div>
</React.Fragment>
)
}

function getStyle(queries, mainClass) {
const reversedQueries = queries.slice().reverse()
const style = reversedQueries
.map(query => {
const currentStyle = `.${mainClass}.${getClassName(
query
)}{display:block}`
const otherStyle = `.${mainClass}:not(.${getClassName(
query
)}){display: none;}`

if (query === "default") {
return `${currentStyle}${otherStyle}`
} else {
return `@media ${query}{${currentStyle}${otherStyle}}`
}
})
.join("\n")
return style
function getStyle(query, mainClass) {
return `.${mainClass}.ssmq-dynamic { display: block; }
.${mainClass}.ssmq-static { display: none; }
@media ${query} {
.${mainClass}.ssmq-dynamic { display: none; }
.${mainClass}.ssmq-static { display: block; }
}
`
}

function getScript(match, mainClass) {
const queries = match.map(([query]) => query)
const classes = queries.map(getClassName)
function getScript(query, mainClass) {
return `(function() {
var qs = ${JSON.stringify(queries)};
var clss = ${JSON.stringify(classes)};
var q = ${JSON.stringify(query)};
var mainCls = "${mainClass}";

var scrEls = document.getElementsByTagName("script");
var scrEl = scrEls[scrEls.length - 1];
var parent = scrEl.parentNode;
var dynamicEl = document.querySelector(
"." + mainCls + ".ssmq-dynamic"
)
var staticEl = document.querySelector(
"." + mainCls + ".ssmq-static"
)
var parent = dynamicEl.parentNode

var el = null;
for (var i = 0; i < qs.length - 1; i++) {
if (window.matchMedia(qs[i]).matches) {
el = parent.querySelector(":scope > ." + mainCls + "." + clss[i]);
break;
}
if (window.matchMedia(q).matches || localStorage.getItem("${PREFERS_STATIC_KEY}") === 'true') {
staticEl.removeAttribute("class")
} else {
dynamicEl.removeAttribute("class")
}
if (!el) {
var defaultClass = clss.pop();
el = parent.querySelector(":scope > ." + mainCls + "." + defaultClass);
}
el.removeAttribute("class");

parent.querySelectorAll(":scope > ." + mainCls).forEach(function (e) {
parent.removeChild(e);
});
parent
.querySelectorAll(":scope > ." + mainCls)
.forEach(function (e) {
parent.removeChild(e)
})
})();`
}

function getClassName(string) {
return (
"ssmq-" +
string
.replace(
/[!\"#$%&'\(\)\*\+,\.\/:;<=>\?\@\[\\\]\^`\{\|\}~\s]/g,
""
)
.toLowerCase()
)
}

function useMedia(queries) {
function useMedia(query) {
const isServer = typeof window === "undefined"

const allQueries = queries.slice(0, -1)

if (queries[queries.length - 1] !== "default") {
console.warn("last media query should be 'default'")
if (isServer) {
return { isServer, showStatic: false }
}

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

const mediaQueryLists = isServer
? []
: allQueries.map(q => window.matchMedia(q))

const mql = window.matchMedia(query)
React.useEffect(() => {
const handler = () => setValue(x => x + 1)
mediaQueryLists.forEach(mql => mql.addListener(handler))
return () =>
mediaQueryLists.forEach(mql =>
mql.removeListener(handler)
)
mql.addEventListener("change", handler)
window.addEventListener("storage", event => {
if (event.key === PREFERS_STATIC_KEY) {
handler()
}
})
return () => {
mql.removeEventListener("change", handler)
window.removeEventListener("storage", handler)
}
}, [])

const matchedIndex = mediaQueryLists.findIndex(
mql => mql.matches
)
const showStatic =
mql.matches ||
localStorage.getItem(PREFERS_STATIC_KEY) === "true"

return {
isServer,
matchedIndex:
matchedIndex < 0 ? queries.length - 1 : matchedIndex,
showStatic,
}
}