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

feat: improvements #1

Merged
merged 10 commits into from
Apr 15, 2020
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
3 changes: 1 addition & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ dist
.nuxt
coverage

# Plugin
lib/templates/*
*.min.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ node_modules
.DS_Store
coverage
dist
*.min.js
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ yarn add --dev @nuxtjs/color-mode
## Usage

It injects `$colorMode` helper with:
- `preference`: actual color-mode selected (can be `'system'`), update it to change the user prefered color mode
- `value`: useful to know what color mode has been detected when `$colorMode === 'system'`, you should not update it
- `preference`: Actual color-mode selected (can be `'system'`), update it to change the user prefered color mode
- `value`: Useful to know what color mode has been detected when `$colorMode === 'system'`, you should not update it
- `unknown`: Useful to know if during SSR or Generate, we need to render a placeholder

```vue
<template>
Expand Down Expand Up @@ -96,6 +97,9 @@ You can configure the module by providing the `colorMode` property in your `nuxt
colorMode: {
preference: 'system', // default value of $colorMode.preference
fallback: 'light', // fallback value if not system preference found
hid: 'nuxt-color-mode-script',
globalName: '__NUXT_COLOR_MODE__',
componentName: 'ColorScheme',
cookie: {
key: 'nuxt-color-mode',
options: {
Expand All @@ -111,7 +115,20 @@ Notes:

## Caveats

With `nuxt generate` and using `$colorMode` in your Vue template, you may expect a flash. This is due to the fact that we cannot know the user preferences when pre-rendering the page, it will directly set the `fallback` value (or `default` value if no set to `'system'`).
If you are doing SSR (`nuxt start` or `nuxt generate`) and if `$colorMode.preference` is set to `'system'`, using `$colorMode` in your Vue template will lead to a flash. This is due to the fact that we cannot know the user preferences when pre-rendering the page since they are detected on client-side.

You have to guard any rendering path which depends on `$colorMode` with `$colorMode.unknown` to render a placeholder or directory use our `<ColorScheme>` component.

***Example:**

```vue
<template>
<ColorScheme placeholder="..." tag="span">
Color mode: <b>{{ $colorMode.preference }}</b>
<span v-if="$colorMode.preference === 'system'">(<i>{{ $colorMode.value }}</i> mode detected)</span>
</ColorScheme>
</template>
```

## TailwindCSS Dark Mode

Expand Down
6 changes: 3 additions & 3 deletions example/components/ColorModePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
</li>
</ul>
<p>
<client-only placeholder="..." placeholder-tag="span">
<color-scheme placeholder="..." tag="span">
Color mode: <b>{{ $colorMode.preference }}</b>
<span v-if="$colorMode.preference === 'system'">(<i>{{ $colorMode.value }}</i> mode detected)</span>
</client-only>
</color-scheme>
</p>
</div>
</template>
Expand All @@ -39,7 +39,7 @@ export default {
methods: {
getClasses (color) {
// Does not set classes on ssr preference is system (because we know them on client-side)
if (process.server && this.$colorMode.preference === 'system') {
if (this.$colorMode.unknown) {
return {}
}
return {
Expand Down
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ module.exports = {
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: [
'lib/**/*.js',
'!lib/templates/**/*.js'
'lib/module.js',
'lib/utils.js'
],
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/lib/$1',
Expand Down
47 changes: 21 additions & 26 deletions lib/module.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
const { readFile } = require('fs').promises
const { resolve } = require('path')
const defu = require('defu')
const template = require('lodash.template')
import { resolve } from 'path'
import { promises as fsp } from 'fs'
import defu from 'defu'
import template from 'lodash.template'
import { addTemplates } from './utils'

module.exports = async function (moduleOptions) {
export default async function (moduleOptions) {
const defaults = {
preference: 'system',
fallback: 'light',
hid: 'nuxt-color-mode-script',
globalName: '__NUXT_COLOR_MODE__',
componentName: 'ColorScheme',
cookie: {
key: 'nuxt-color-mode',
options: {
Expand All @@ -20,32 +24,23 @@ module.exports = async function (moduleOptions) {
...moduleOptions
}, defaults)

// Add plugin to inject $colorMode
this.addPlugin({
src: resolve(__dirname, 'templates', 'plugins', 'color-mode.client.js'),
fileName: 'plugins/color-mode.client.js',
options
})
this.addPlugin({
src: resolve(__dirname, 'templates', 'plugins', 'color-mode.server.js'),
fileName: 'plugins/color-mode.server.js',
options
})
// Add all templates
const templatesDir = resolve(__dirname, 'templates')
await addTemplates.call(this, templatesDir, 'color-mode', options)

// Add script to head to detect user or system preference before loading Nuxt (for SSR)
let script = await readFile(resolve(__dirname, 'templates', 'script.js'), 'utf-8')
script = template(script)({ options })
// minify script for production
if (!this.options.dev) {
const { minify } = require('terser')
script = minify(script).code
}
const scriptPath = resolve(__dirname, 'script.min.js')
const scriptT = await fsp.readFile(scriptPath, 'utf-8')
const script = template(scriptT)({ options })

this.options.head.script = this.options.head.script || []
this.options.head.script.push({
hid: 'nuxt-color-mode-script',
hid: options.hid,
innerHTML: script,
pbody: true
})
this.options.head.__dangerouslyDisableSanitizersByTagID = this.options.head.__dangerouslyDisableSanitizersByTagID || {}
this.options.head.__dangerouslyDisableSanitizersByTagID['nuxt-color-mode-script'] = ['innerHTML']

const serializeProp = '__dangerouslyDisableSanitizersByTagID'
this.options.head[serializeProp] = this.options.head[serializeProp] || {}
this.options.head[serializeProp][options.hid] = ['innerHTML']
}
71 changes: 71 additions & 0 deletions lib/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Add dark / light detection that runs before loading Nuxt.js

// Global variable minimizers
const w = window
const d = document
const de = d.documentElement

const knownColorSchemes = ['dark', 'light']

const preference = getCookie('<%= options.cookie.key %>') || '<%= options.preference %>'
const value = preference === 'system' ? getColorScheme() : preference

addClass(value)

w['<%= options.globalName %>'] = {
preference,
value,
getColorScheme,
addClass,
removeClass
}

function getCookie (name) {
const nameEQ = name + '='
const cookies = d.cookie.split(';')

for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i]
while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1, cookie.length)
}
if (cookie.indexOf(nameEQ) === 0) {
return cookie.substring(nameEQ.length, cookie.length)
}
}
return null
}

function addClass (value) {
const className = value + '-mode'
if (de.classList) {
de.classList.add(className)
} else {
de.className += ' ' + className
}
}

function removeClass (value) {
const className = value + '-mode'
if (de.classList) {
de.classList.remove(className)
} else {
de.className = de.className.replace(new RegExp(className, 'g'), '')
}
}

function prefersColorScheme (suffix) {
return w.matchMedia('(prefers-color-scheme' + suffix + ')')
}

function getColorScheme () {
if (w.matchMedia && prefersColorScheme('').media !== 'not all') {
for (const colorScheme of knownColorSchemes) {
if (prefersColorScheme(':' + colorScheme).matches) {
return colorScheme
}
}
}

return '<%= options.fallback %>'
}
17 changes: 17 additions & 0 deletions lib/templates/color-scheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default {
name: '<%= options.componentName %>',
functional: true,
render (createElement, context) {
const tag = context.tag || 'span'
const { $colorMode } = context.parent

if (!$colorMode.unknown) {
return createElement(tag, context.data, context.children)
}

return createElement('client-only', {
'placeholder-tag': tag,
...context.data
}, context.children)
}
}
75 changes: 75 additions & 0 deletions lib/templates/plugin.client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Vue from 'vue'
import { serialize } from 'cookie'
import colorSchemeComponent from './color-scheme'

Vue.component('<%= options.componentName %>', colorSchemeComponent)

const cookieKey = '<%= options.cookie.key %>'
const cookieOptions = JSON.parse('<%= JSON.stringify(options.cookie.options) %>')
const colorMode = window['<%= options.globalName %>']

export default function (ctx, inject) {
const $colorMode = new Vue({
data: {
preference: colorMode.preference,
value: colorMode.value,
unknown: false
},
watch: {
preference (preference) {
if (preference === 'system') {
this.value = colorMode.getColorScheme()
this._watchMedia()
} else {
this.value = preference
}

this._storePreference(preference)
},
value (newValue, oldValue) {
colorMode.removeClass(oldValue)
colorMode.addClass(newValue)
}
},
created () {
if (this.preference === 'system') {
this._watchMedia()
}
if (window.localStorage) {
this._watchStorageChange()
}
},
methods: {
_watchMedia () {
if (this._mediaWatcher || !window.matchMedia) {
return
}

this._darkWatcher = window.matchMedia('(prefers-color-scheme: dark)')
this._darkWatcher.addListener((e) => {
if (this.preference === 'system') {
this.value = colorMode.getColorScheme()
}
})
},
_watchStorageChange () {
window.addEventListener('storage', (e) => {
if (e.key === cookieKey) {
this.preference = e.newValue
}
})
},
_storePreference (preference) {
// Cookies for SSR
document.cookie = serialize(cookieKey, preference, cookieOptions)

// Local storage to sync with other tabs
if (window.localStorage) {
window.localStorage.setItem(cookieKey, preference)
}
}
}
})

inject('colorMode', $colorMode)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import Vue from 'vue'
import { parse } from 'cookie'

import colorSchemeComponent from './color-scheme'
const cookieKey = '<%= options.cookie.key %>'

Vue.component('<%= options.componentName %>', colorSchemeComponent)

export default function (ctx, inject) {
// Read from cookie
let preference = '<%= options.preference %>'

// Try to read from cookies
if (ctx.req) {
// Check if cookie exist, otherwise TypeError: argument str must be a string
const cookies = parse(ctx.req.headers.cookie || '')

if (cookies[cookieKey] && cookies[cookieKey] !== preference) {
if (cookies[cookieKey]) {
preference = cookies[cookieKey]
}
}

const colorMode = {
preference: preference,
value: preference
preference,
value: preference,
unknown: process.static || !ctx.req || preference === 'system'
}

inject('colorMode', colorMode)
Expand Down
Loading