33import React from "react"
44
55let 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