Skip to content

Commit

Permalink
Merge pull request #395 from code-hike/static-toggle
Browse files Browse the repository at this point in the history
Add static toggle
  • Loading branch information
pomber authored Jul 4, 2023
2 parents e0512c1 + 984614e commit 111f44a
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 109 deletions.
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,
}
}

0 comments on commit 111f44a

Please sign in to comment.