3
3
import React from "react"
4
4
5
5
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
+ }
6
56
7
57
/**
8
58
* @typedef SwapProps
@@ -14,139 +64,110 @@ let suffixCounter = 0
14
64
* @param {SwapProps } props
15
65
*/
16
66
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 )
20
71
const mainClassName = isServer
21
72
? "ssmq-" + suffixCounter ++
22
73
: ""
23
-
24
74
return isServer ? (
25
75
< React . Fragment >
26
76
< style
27
77
className = { mainClassName }
28
78
dangerouslySetInnerHTML = { {
29
- __html : getStyle ( queries , mainClassName ) ,
79
+ __html : getStyle ( query , mainClassName ) ,
30
80
} }
31
81
/>
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 >
42
88
< script
43
89
className = { mainClassName }
44
90
dangerouslySetInnerHTML = { {
45
- __html : getScript ( match , mainClassName ) ,
91
+ __html : getScript ( query , mainClassName ) ,
46
92
} }
47
93
/>
48
94
</ React . Fragment >
49
95
) : (
50
96
< React . Fragment >
51
- < div > { match [ matchedIndex ] [ 1 ] } </ div >
97
+ < div >
98
+ { showStatic ? staticElement : dynamicElement }
99
+ </ div >
52
100
</ React . Fragment >
53
101
)
54
102
}
55
103
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
+ `
75
112
}
76
113
77
- function getScript ( match , mainClass ) {
78
- const queries = match . map ( ( [ query ] ) => query )
79
- const classes = queries . map ( getClassName )
114
+ function getScript ( query , mainClass ) {
80
115
return `(function() {
81
- var qs = ${ JSON . stringify ( queries ) } ;
82
- var clss = ${ JSON . stringify ( classes ) } ;
116
+ var q = ${ JSON . stringify ( query ) } ;
83
117
var mainCls = "${ mainClass } ";
84
118
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
88
126
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")
95
131
}
96
- if (!el) {
97
- var defaultClass = clss.pop();
98
- el = parent.querySelector(":scope > ." + mainCls + "." + defaultClass);
99
- }
100
- el.removeAttribute("class");
101
132
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
+ })
105
138
})();`
106
139
}
107
140
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 ) {
121
142
const isServer = typeof window === "undefined"
122
143
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 }
127
146
}
128
147
129
148
const [ , setValue ] = React . useState ( 0 )
130
149
131
- const mediaQueryLists = isServer
132
- ? [ ]
133
- : allQueries . map ( q => window . matchMedia ( q ) )
134
-
150
+ const mql = window . matchMedia ( query )
135
151
React . useEffect ( ( ) => {
136
152
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
+ }
142
163
} , [ ] )
143
164
144
- const matchedIndex = mediaQueryLists . findIndex (
145
- mql => mql . matches
146
- )
165
+ const showStatic =
166
+ mql . matches ||
167
+ localStorage . getItem ( PREFERS_STATIC_KEY ) === "true"
168
+
147
169
return {
148
170
isServer,
149
- matchedIndex :
150
- matchedIndex < 0 ? queries . length - 1 : matchedIndex ,
171
+ showStatic,
151
172
}
152
173
}
0 commit comments