|
1 | 1 | <template>
|
2 | 2 | <button
|
3 |
| - :aria-pressed="darkMode" |
| 3 | + :aria-label="`toggle to ${getNextMode} mode color`" |
| 4 | + :title="`toggle to ${getNextMode} mode color`" |
4 | 5 | class="vue-dark-mode"
|
5 |
| - @click="toggleDarkMode" |
| 6 | + @click="toggleColorMode" |
6 | 7 | >
|
7 |
| - <slot :isDark="darkMode" /> |
8 |
| - <component |
9 |
| - :is="'style'" |
10 |
| - :media="darkMode && $_use_filter ? 'screen' : 'none'" |
11 |
| - scoped |
12 |
| - v-text="styles.trim()" |
13 |
| - /> |
| 8 | + <span |
| 9 | + class="visually-hidden" |
| 10 | + aria-live="assertive" |
| 11 | + > |
| 12 | + {{ chosenMode }} color mode is enabled |
| 13 | + </span> |
| 14 | + <slot :mode="chosenMode" /> |
14 | 15 | </button>
|
15 | 16 | </template>
|
16 | 17 |
|
17 | 18 | <script>
|
18 |
| -const styles = ` |
19 |
| - html { |
20 |
| - background-color: #222 !important; |
21 |
| - color: #333 !important; |
22 |
| - } |
23 |
| -
|
24 |
| - body { |
25 |
| - filter: contrast(90%) invert(90%) hue-rotate(180deg) !important; |
26 |
| - -ms-filter: invert(100%); |
27 |
| - -webkit-filter: contrast(90%) invert(90%) hue-rotate(180deg) !important; |
28 |
| - } |
29 |
| -
|
30 |
| - input, textarea, select { |
31 |
| - color: purple; |
32 |
| - } |
33 |
| -
|
34 |
| - img, video, iframe, canvas, svg, embed[type='application/x-shockwave-flash'], object[type='application/x-shockwave-flash'], *[style*='url('] { |
35 |
| - filter: invert(100%) hue-rotate(-180deg) !important; |
36 |
| - -ms-filter: invert(100%) !important; |
37 |
| - -webkit-filter: invert(100%) hue-rotate(-180deg) !important; |
38 |
| - } |
39 |
| -` |
40 |
| -
|
41 | 19 | export default {
|
42 | 20 | name: 'DarkMode',
|
43 | 21 |
|
44 | 22 | props: {
|
45 |
| - isDark: { |
46 |
| - type: Boolean, |
47 |
| - default: false |
48 |
| - }, |
49 |
| - useFilter: { |
50 |
| - type: Boolean, |
51 |
| - default: true |
52 |
| - }, |
53 |
| - styles: { |
| 23 | + defaultMode: { |
54 | 24 | type: String,
|
55 |
| - default: styles |
| 25 | + default: 'light' |
56 | 26 | },
|
57 |
| - className: { |
58 |
| - type: String, |
59 |
| - default: 'dark-mode' |
| 27 | + modes: { |
| 28 | + type: Array, |
| 29 | + default () { |
| 30 | + return ['light', 'dark', 'system'] |
| 31 | + } |
60 | 32 | },
|
61 |
| - persist: { |
| 33 | + storage: { |
62 | 34 | type: String,
|
63 | 35 | default: 'localStorage'
|
64 | 36 | },
|
65 |
| - themeColorLight: { |
66 |
| - type: String, |
67 |
| - default: '#f2f2f2' |
68 |
| - }, |
69 |
| - themColorDark: { |
70 |
| - type: String, |
71 |
| - default: '#999' |
| 37 | + mobileThemeColor: { |
| 38 | + type: Object, |
| 39 | + default () { |
| 40 | + return { |
| 41 | + light: '#f2f2f2', |
| 42 | + dark: '#999' |
| 43 | + } |
| 44 | + } |
72 | 45 | }
|
73 | 46 | },
|
74 | 47 |
|
75 | 48 | data () {
|
76 | 49 | return {
|
77 |
| - darkMode: false, |
78 | 50 | themeColorMeta: null,
|
79 |
| - $_use_filter: false |
| 51 | + chosenMode: null, |
| 52 | + currentMode: null, |
| 53 | + listenerDark: null |
80 | 54 | }
|
81 | 55 | },
|
82 | 56 |
|
83 |
| - created () { |
84 |
| - this.darkMode = this.isDark |
85 |
| - if (!this.$isServer) { |
86 |
| - this.darkMode = !!window[this.persist].getItem('darkMode') || this.isDark || this.prefersDark() |
87 |
| - this.themeColorMeta = document.querySelector('meta[name="theme-color"]') |
88 |
| - if (this.useFilter) { |
89 |
| - this.$_use_filter = this.supportsFilters() |
90 |
| - } |
91 |
| - if (this.darkMode) this.setDarkMode() |
| 57 | + computed: { |
| 58 | + getPrefersColorScheme () { |
| 59 | + const colorSchemeTypes = ['dark', 'light'] |
| 60 | + let colorScheme = null |
| 61 | + colorSchemeTypes.forEach(type => { |
| 62 | + if (this.getMediaQueryList(type).matches) { |
| 63 | + colorScheme = type |
| 64 | + } |
| 65 | + }) |
| 66 | + return colorScheme |
| 67 | + }, |
| 68 | +
|
| 69 | + getNextMode () { |
| 70 | + const currentIndex = this.modes.findIndex(mode => mode === this.chosenMode) |
| 71 | + return this.modes[currentIndex === (this.modes.length - 1) ? 0 : currentIndex + 1] |
| 72 | + }, |
| 73 | +
|
| 74 | + getStorageColorMode () { |
| 75 | + return window[this.storage].getItem('colorMode') |
| 76 | + }, |
| 77 | +
|
| 78 | + isSystem () { |
| 79 | + return this.getStorageColorMode === 'system' |
92 | 80 | }
|
93 | 81 | },
|
94 | 82 |
|
95 |
| - methods: { |
96 |
| - supportsFilters () { |
97 |
| - const div = document.createElement('div') |
98 |
| - const isSupported = 'filter' in div.style |
99 |
| - if (!isSupported) console.warn('CSS filter is not supported') |
100 |
| - return isSupported |
101 |
| - }, |
| 83 | + beforeMount () { |
| 84 | + if (this.getPrefersColorScheme && this.isSystem) { |
| 85 | + this.currentMode = this.getPrefersColorScheme |
| 86 | + return this.setMode('system') |
| 87 | + } |
| 88 | + const colorMode = this.getStorageColorMode || this.defaultMode |
| 89 | + this.currentMode = colorMode |
| 90 | + this.setMode(colorMode) |
| 91 | + }, |
102 | 92 |
|
103 |
| - prefersDark () { |
104 |
| - return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches |
| 93 | + mounted () { |
| 94 | + this.metaThemeColor = document.querySelector('meta[name="theme-color"]') |
| 95 | + this.listenerDark = this.getMediaQueryList('dark') |
| 96 | + this.listenerDark.addListener(this.handlePreferColorScheme) |
| 97 | + }, |
| 98 | +
|
| 99 | + beforeDestroy () { |
| 100 | + this.listenerDark.removeListener(this.handlePreferColorScheme) |
| 101 | + }, |
| 102 | +
|
| 103 | + methods: { |
| 104 | + setMode (chosenMode) { |
| 105 | + this.chosenMode = chosenMode |
| 106 | + window[this.storage].setItem('colorMode', this.chosenMode) |
| 107 | + this.handleClassList('add', `${this.currentMode}-mode`) |
| 108 | + this.setMetaThemeColor(this.mobileThemeColor[this.currentMode] || this.mobileThemeColor[this.getPrefersColorScheme] || '#fff') |
105 | 109 | },
|
106 | 110 |
|
107 |
| - setThemeColor (color) { |
108 |
| - if (this.themeColorMeta) return this.themeColorMeta.setAttribute('content', color) |
| 111 | + getMediaQueryList (type) { |
| 112 | + return window.matchMedia(`(prefers-color-scheme: ${type})`) |
109 | 113 | },
|
110 | 114 |
|
111 |
| - toggleClass () { |
112 |
| - document.documentElement.classList.toggle(this.className) |
| 115 | + setMetaThemeColor (color) { |
| 116 | + this.$nextTick(() => { |
| 117 | + if (this.metaThemeColor) this.metaThemeColor.setAttribute('content', color) |
| 118 | + }) |
113 | 119 | },
|
114 | 120 |
|
115 |
| - setDarkMode () { |
116 |
| - window[this.persist].setItem('darkMode', 'on') |
117 |
| - this.toggleClass() |
118 |
| - this.setThemeColor(this.themeColorDark) |
| 121 | + handleClassList (action, cls) { |
| 122 | + return document.documentElement.classList[action](cls) |
119 | 123 | },
|
120 | 124 |
|
121 |
| - removeDarkMode () { |
122 |
| - window[this.persist].removeItem('darkMode') |
123 |
| - this.toggleClass() |
124 |
| - this.setThemeColor(this.themeColorLight) |
| 125 | + handlePreferColorScheme (e) { |
| 126 | + this.currentMode = this.isSystem && e.matches ? 'dark' : 'light' |
| 127 | + this.handleClassList('remove', `${this.currentMode}-mode`) |
| 128 | + this.setMode('system') |
125 | 129 | },
|
126 | 130 |
|
127 |
| - toggleDarkMode () { |
128 |
| - this.darkMode = !this.darkMode |
129 |
| - if (this.darkMode) return this.setDarkMode() |
130 |
| - this.removeDarkMode() |
| 131 | + toggleColorMode () { |
| 132 | + const selectedMode = this.getNextMode |
| 133 | + this.handleClassList('remove', `${this.currentMode}-mode`) |
| 134 | + this.currentMode = selectedMode === 'system' ? this.getPrefersColorScheme : selectedMode |
| 135 | + this.setMode(selectedMode) |
131 | 136 | }
|
132 | 137 | }
|
133 | 138 | }
|
134 | 139 | </script>
|
135 | 140 |
|
136 | 141 | <style>
|
137 |
| -body { |
138 |
| - text-rendering: optimizeSpeed; |
139 |
| - image-rendering: optimizeSpeed; |
140 |
| - -webkit-font-smoothing: antialiased; |
141 |
| - -webkit-image-rendering: optimizeSpeed; |
142 |
| -} |
143 |
| -
|
144 | 142 | .vue-dark-mode {
|
145 | 143 | appearance: none;
|
146 | 144 | background-color: transparent;
|
147 | 145 | color: inherit;
|
148 | 146 | border: none;
|
149 | 147 | cursor: pointer;
|
150 | 148 | }
|
| 149 | +
|
| 150 | +.visually-hidden { |
| 151 | + position: absolute; |
| 152 | + white-space: nowrap; |
| 153 | + width: 1px; |
| 154 | + height: 1px; |
| 155 | + overflow: hidden; |
| 156 | + border: 0; |
| 157 | + padding: 0; |
| 158 | + clip: rect(0 0 0 0); |
| 159 | + clip-path: inset(50%); |
| 160 | + margin: -1px; |
| 161 | +} |
151 | 162 | </style>
|
0 commit comments