This commit is contained in:
darenhsu
2022-07-17 13:16:16 +08:00
parent 84759556ff
commit befd344ab0
28070 changed files with 4008428 additions and 1 deletions
@@ -0,0 +1,29 @@
// Services
import { Application } from '../index'
describe('Application.ts', () => {
let app: Application
beforeEach(() => {
app = new Application()
})
it('should register/unregister value on application section', () => {
app.register(0, 'bar', 56)
expect(app.bar).toBe(56)
app.unregister(0, 'bar')
expect(app.bar).toBe(0)
})
it('should not update if value doesn\'t exist in application', () => {
const spy = jest.spyOn(app, 'update')
app.register(0, 'top', 24)
app.unregister(1, 'top')
expect(spy).toHaveBeenCalledTimes(1)
})
})
+55
View File
@@ -0,0 +1,55 @@
// Extensions
import { Service } from '../service'
// Types
import { TargetPropValues, TargetProp, Application as IApplication } from 'vuetify/types/services/application'
export class Application extends Service implements IApplication {
static property: 'application' = 'application'
bar = 0
top = 0
left = 0
insetFooter = 0
right = 0
bottom = 0
footer = 0
application: Dictionary<TargetPropValues> = {
bar: {},
top: {},
left: {},
insetFooter: {},
right: {},
bottom: {},
footer: {},
}
register (
uid: number,
location: TargetProp,
size: number
) {
this.application[location] = { [uid]: size }
this.update(location)
}
unregister (uid: number, location: TargetProp) {
if (this.application[location][uid] == null) return
delete this.application[location][uid]
this.update(location)
}
update (location: TargetProp) {
this[location] = Object.values(this.application[location])
.reduce((acc: number, cur: number): number => (acc + cur), 0)
}
}
@@ -0,0 +1,298 @@
import { resizeWindow } from '../../../../test'
import { Breakpoint } from '../'
import { preset } from '../../../presets/default'
describe('Breakpoint.ts', () => {
let breakpoint: Breakpoint
const scenarios = [
{
description: 'Huawei Smartwatch',
width: 400,
height: 400,
name: 'xs',
mustBeTrue: [
'xs',
'xsOnly',
'smAndDown',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'Galaxy S5 (portrait)',
width: 360,
height: 640,
name: 'xs',
mustBeTrue: [
'xs',
'xsOnly',
'smAndDown',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'Galaxy S5 (landscape)',
width: 640,
height: 360,
name: 'sm',
mustBeTrue: [
'sm',
'smOnly',
'smAndDown',
'smAndUp',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'iPhone 6 (portrait)',
width: 375,
height: 667,
name: 'xs',
mustBeTrue: [
'xs',
'xsOnly',
'smAndDown',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'iPhone 6 (landscape)',
width: 667,
height: 375,
name: 'sm',
mustBeTrue: [
'sm',
'smOnly',
'smAndDown',
'smAndUp',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'iPad (portrait)',
width: 768,
height: 1024,
name: 'sm',
mustBeTrue: [
'sm',
'smOnly',
'smAndDown',
'smAndUp',
'mdAndDown',
'lgAndDown',
],
},
{
description: 'iPad (landscape)',
width: 1024,
height: 768,
name: 'md',
mustBeTrue: [
'md',
'mdOnly',
'smAndUp',
'mdAndDown',
'mdAndUp',
'lgAndDown',
],
},
{
description: 'iPad Pro (portrait)',
width: 1024,
height: 1366,
name: 'md',
mustBeTrue: [
'md',
'mdOnly',
'smAndUp',
'mdAndDown',
'mdAndUp',
'lgAndDown',
],
},
{
description: 'iPad Pro (landscape)',
width: 1366,
height: 1024,
name: 'lg',
mustBeTrue: [
'lg',
'lgOnly',
'smAndUp',
'mdAndUp',
'lgAndDown',
'lgAndUp',
],
},
{
description: 'WSXGA+ (portrait)',
width: 1050,
height: 1680,
name: 'md',
mustBeTrue: [
'md',
'mdOnly',
'smAndUp',
'mdAndDown',
'mdAndUp',
'lgAndDown',
],
},
{
description: 'WSXGA+ (landscape)',
width: 1680,
height: 1050,
name: 'lg',
mustBeTrue: [
'lg',
'lgOnly',
'smAndUp',
'mdAndUp',
'lgAndDown',
'lgAndUp',
],
},
{
description: 'FHD (portrait)',
width: 1080,
height: 1920,
name: 'md',
mustBeTrue: [
'md',
'mdOnly',
'smAndUp',
'mdAndDown',
'mdAndUp',
'lgAndDown',
],
},
{
description: 'FHD (landscape)',
width: 1920,
height: 1080,
name: 'xl',
mustBeTrue: [
'xl',
'xlOnly',
'smAndUp',
'mdAndUp',
'lgAndUp',
],
},
{
description: 'WQHD (portrait)',
width: 1440,
height: 2560,
name: 'lg',
mustBeTrue: [
'lg',
'lgOnly',
'smAndUp',
'mdAndUp',
'lgAndDown',
'lgAndUp',
],
},
{
description: 'WQHD (landscape)',
width: 2560,
height: 1440,
name: 'xl',
mustBeTrue: [
'xl',
'xlOnly',
'smAndUp',
'mdAndUp',
'lgAndUp',
],
},
]
const allFlags = [
'xs',
'sm',
'md',
'lg',
'xl',
'xsOnly',
'smOnly',
'smAndDown',
'smAndUp',
'mdOnly',
'mdAndDown',
'mdAndUp',
'lgOnly',
'lgAndDown',
'lgAndUp',
'xlOnly',
]
beforeEach(() => {
breakpoint = new Breakpoint(preset)
})
scenarios.slice(0, 1).forEach(scenario => {
it('should calculate breakpoint for ' + scenario.description, async () => {
await resizeWindow(scenario.width, scenario.height)
expect(breakpoint.width).toBe(scenario.width)
expect(breakpoint.height).toBe(scenario.height)
expect(breakpoint.name).toBe(scenario.name)
allFlags.forEach(flag => {
const expectedValue = scenario.mustBeTrue.indexOf(flag) !== -1
expect(breakpoint[flag]).toBe(expectedValue)
})
})
})
it('should update breakpoint on window resize', async () => {
await resizeWindow(715)
expect(breakpoint.width).toBe(715)
})
it('should allow to change thresholds during runtime', async () => {
await resizeWindow(401)
expect(breakpoint.xs).toBe(true)
await resizeWindow(698)
expect(breakpoint.xs).toBe(false)
await resizeWindow(1099)
expect(breakpoint.md).toBe(true)
await resizeWindow(1281)
expect(breakpoint.lg).toBe(true)
await resizeWindow(1921)
expect(breakpoint.xl).toBe(true)
})
it('should allow to override defaults via factory args', async () => {
breakpoint = new Breakpoint({
...preset,
breakpoint: {
thresholds: { xs: 400 },
} as any,
})
await resizeWindow(401)
expect(breakpoint.xs).toBe(false)
await resizeWindow(399)
expect(breakpoint.xs).toBe(true)
})
it('should allow breakpoint strings for mobileBreakpoint', async () => {
breakpoint.mobileBreakpoint = 'lg'
await resizeWindow(1920)
expect(breakpoint.mobile).toBe(false)
await resizeWindow(600)
expect(breakpoint.mobile).toBe(true)
})
})
+191
View File
@@ -0,0 +1,191 @@
// Extensions
import { Service } from '../service'
// Types
import { VuetifyPreset } from 'vuetify/types/services/presets'
import { Breakpoint as IBreakpoint } from 'vuetify/types/services/breakpoint'
export class Breakpoint extends Service implements IBreakpoint {
public static property: 'breakpoint' = 'breakpoint'
// Public
public xs = false
public sm = false
public md = false
public lg = false
public xl = false
public xsOnly = false
public smOnly = false
public smAndDown = false
public smAndUp = false
public mdOnly = false
public mdAndDown = false
public mdAndUp = false
public lgOnly = false
public lgAndDown = false
public lgAndUp = false
public xlOnly = false
// Value is xs to match v2.x functionality
public name: IBreakpoint['name'] = 'xs'
public height = 0
public width = 0
// TODO: Add functionality to detect this dynamically in v3
// Value is true to match v2.x functionality
public mobile = true
public mobileBreakpoint: IBreakpoint['mobileBreakpoint']
public thresholds: IBreakpoint['thresholds']
public scrollBarWidth: IBreakpoint['scrollBarWidth']
private resizeTimeout = 0
constructor (preset: VuetifyPreset) {
super()
const {
mobileBreakpoint,
scrollBarWidth,
thresholds,
} = preset[Breakpoint.property]
this.mobileBreakpoint = mobileBreakpoint
this.scrollBarWidth = scrollBarWidth
this.thresholds = thresholds
this.init()
}
public init () {
/* istanbul ignore if */
if (typeof window === 'undefined') return
window.addEventListener(
'resize',
this.onResize.bind(this),
{ passive: true }
)
this.update()
}
private onResize () {
clearTimeout(this.resizeTimeout)
// Added debounce to match what
// v-resize used to do but was
// removed due to a memory leak
// https://github.com/vuetifyjs/vuetify/pull/2997
this.resizeTimeout = window.setTimeout(this.update.bind(this), 200)
}
/* eslint-disable-next-line max-statements */
private update () {
const height = this.getClientHeight()
const width = this.getClientWidth()
const xs = width < this.thresholds.xs
const sm = width < this.thresholds.sm && !xs
const md = width < (this.thresholds.md - this.scrollBarWidth) && !(sm || xs)
const lg = width < (this.thresholds.lg - this.scrollBarWidth) && !(md || sm || xs)
const xl = width >= (this.thresholds.lg - this.scrollBarWidth)
this.height = height
this.width = width
this.xs = xs
this.sm = sm
this.md = md
this.lg = lg
this.xl = xl
this.xsOnly = xs
this.smOnly = sm
this.smAndDown = (xs || sm) && !(md || lg || xl)
this.smAndUp = !xs && (sm || md || lg || xl)
this.mdOnly = md
this.mdAndDown = (xs || sm || md) && !(lg || xl)
this.mdAndUp = !(xs || sm) && (md || lg || xl)
this.lgOnly = lg
this.lgAndDown = (xs || sm || md || lg) && !xl
this.lgAndUp = !(xs || sm || md) && (lg || xl)
this.xlOnly = xl
switch (true) {
case (xs):
this.name = 'xs'
break
case (sm):
this.name = 'sm'
break
case (md):
this.name = 'md'
break
case (lg):
this.name = 'lg'
break
default:
this.name = 'xl'
break
}
if (typeof this.mobileBreakpoint === 'number') {
this.mobile = width < parseInt(this.mobileBreakpoint, 10)
return
}
const breakpoints = {
xs: 0,
sm: 1,
md: 2,
lg: 3,
xl: 4,
} as const
const current = breakpoints[this.name]
const max = breakpoints[this.mobileBreakpoint]
this.mobile = current <= max
}
// Cross-browser support as described in:
// https://stackoverflow.com/questions/1248081
private getClientWidth () {
/* istanbul ignore if */
if (typeof document === 'undefined') return 0 // SSR
return Math.max(
document.documentElement!.clientWidth,
window.innerWidth || 0
)
}
private getClientHeight () {
/* istanbul ignore if */
if (typeof document === 'undefined') return 0 // SSR
return Math.max(
document.documentElement!.clientHeight,
window.innerHeight || 0
)
}
}
@@ -0,0 +1,23 @@
import * as easingPatterns from '../easing-patterns'
describe('easing-patterns.ts', () => {
it('should work', () => {
expect(easingPatterns.linear(5)).toBe(5)
expect(easingPatterns.easeInQuad(5)).toBe(25)
expect(easingPatterns.easeOutQuad(5)).toBe(-15)
expect(easingPatterns.easeInOutQuad(5)).toBe(-31)
expect(easingPatterns.easeInOutQuad(0.1)).toBe(0.020000000000000004)
expect(easingPatterns.easeInCubic(5)).toBe(125)
expect(easingPatterns.easeOutCubic(5)).toBe(65)
expect(easingPatterns.easeInOutCubic(5)).toBe(257)
expect(easingPatterns.easeInOutCubic(0.1)).toBe(0.004000000000000001)
expect(easingPatterns.easeInQuart(5)).toBe(625)
expect(easingPatterns.easeOutQuart(5)).toBe(-255)
expect(easingPatterns.easeInOutQuart(5)).toBe(-2047)
expect(easingPatterns.easeInOutQuart(0.1)).toBe(0.0008000000000000003)
expect(easingPatterns.easeInQuint(5)).toBe(3125)
expect(easingPatterns.easeOutQuint(5)).toBe(1025)
expect(easingPatterns.easeInOutQuint(5)).toBe(16385)
expect(easingPatterns.easeInOutQuint(0.1)).toBeCloseTo(0.00016, 5)
})
})
+69
View File
@@ -0,0 +1,69 @@
// Lib
import { mount } from '@vue/test-utils'
// Components
import VBtn from '../../../components/VBtn'
// Services
import goTo, { Goto } from '../index'
import { Application } from '../../application/index'
// Types
import { VuetifyServiceContract } from 'vuetify/types/services'
describe('$vuetify.goTo', () => {
(global as any).performance = require('perf_hooks').performance
let framework: Record<string, VuetifyServiceContract> = {}
beforeEach(() => {
framework = {
application: new Application(),
}
goTo.framework = framework
})
it('should throw error when target is undefined or null', async () => {
expect(() => goTo(undefined))
.toThrow(new TypeError('Target must be a Number/Selector/HTMLElement/VueComponent, received undefined instead.'))
expect(() => goTo(null))
.toThrow(new TypeError('Target must be a Number/Selector/HTMLElement/VueComponent, received null instead.'))
})
it('should throw error when target element is not found', async () => {
expect(() => goTo('#foo'))
.toThrow(new Error('Target element "#foo" not found.'))
})
it('should throw error when container element is not found', async () => {
expect(() => goTo(0, { container: '#thisContainerDoesNotExist' }))
.toThrow(new Error('Container element "#thisContainerDoesNotExist" not found.'))
})
it('should throw error when container is undefined or null', async () => {
expect(() => goTo(0, { container: undefined }))
.toThrow(new TypeError('Container must be a Selector/HTMLElement/VueComponent, received undefined instead.'))
expect(() => goTo(0, { container: null }))
.toThrow(new TypeError('Container must be a Selector/HTMLElement/VueComponent, received null instead.'))
expect(() => goTo(0, { container: 42 as any }))
.toThrow(new TypeError('Container must be a Selector/HTMLElement/VueComponent, received Number instead.'))
})
it('should throw error if easing does not exist', async () => {
expect(() => goTo(1, { easing: 'thisEasingDoesNotExist' }))
.toThrow(new TypeError('Easing function "thisEasingDoesNotExist" not found.'))
})
it('should not throw error when using VueComponent as target', async () => {
const btn = mount(VBtn)
await expect(goTo(btn.vm, { duration: 0 })).resolves.not.toBe(undefined)
})
it('should instantiate and return goto', () => {
expect(new Goto()).toEqual(goTo)
})
})
+28
View File
@@ -0,0 +1,28 @@
export type EasingFunction = (t: number) => number
// linear
export const linear = (t: number) => t
// accelerating from zero velocity
export const easeInQuad = (t: number) => t ** 2
// decelerating to zero velocity
export const easeOutQuad = (t: number) => t * (2 - t)
// acceleration until halfway, then deceleration
export const easeInOutQuad = (t: number) => (t < 0.5 ? 2 * t ** 2 : -1 + (4 - 2 * t) * t)
// accelerating from zero velocity
export const easeInCubic = (t: number) => t ** 3
// decelerating to zero velocity
export const easeOutCubic = (t: number) => --t ** 3 + 1
// acceleration until halfway, then deceleration
export const easeInOutCubic = (t: number) => t < 0.5 ? 4 * t ** 3 : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
// accelerating from zero velocity
export const easeInQuart = (t: number) => t ** 4
// decelerating to zero velocity
export const easeOutQuart = (t: number) => 1 - --t ** 4
// acceleration until halfway, then deceleration
export const easeInOutQuart = (t: number) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t)
// accelerating from zero velocity
export const easeInQuint = (t: number) => t ** 5
// decelerating to zero velocity
export const easeOutQuint = (t: number) => 1 + --t ** 5
// acceleration until halfway, then deceleration
export const easeInOutQuint = (t: number) => t < 0.5 ? 16 * t ** 5 : 1 + 16 * --t ** 5
+88
View File
@@ -0,0 +1,88 @@
// Extensions
import { Service } from '../service'
// Utilities
import * as easingPatterns from './easing-patterns'
import {
getContainer,
getOffset,
} from './util'
// Types
import { GoToOptions, VuetifyGoToTarget } from 'vuetify/types/services/goto'
import { VuetifyServiceContract } from 'vuetify/types/services'
export default function goTo (
_target: VuetifyGoToTarget,
_settings: Partial<GoToOptions> = {}
): Promise<number> {
const settings: GoToOptions = {
container: (document.scrollingElement as HTMLElement | null) || document.body || document.documentElement,
duration: 500,
offset: 0,
easing: 'easeInOutCubic',
appOffset: true,
..._settings,
}
const container = getContainer(settings.container)
/* istanbul ignore else */
if (settings.appOffset && goTo.framework.application) {
const isDrawer = container.classList.contains('v-navigation-drawer')
const isClipped = container.classList.contains('v-navigation-drawer--clipped')
const { bar, top } = goTo.framework.application as any
settings.offset += bar
/* istanbul ignore else */
if (!isDrawer || isClipped) settings.offset += top
}
const startTime = performance.now()
let targetLocation: number
if (typeof _target === 'number') {
targetLocation = getOffset(_target) - settings.offset!
} else {
targetLocation = getOffset(_target) - getOffset(container) - settings.offset!
}
const startLocation = container.scrollTop
if (targetLocation === startLocation) return Promise.resolve(targetLocation)
const ease = typeof settings.easing === 'function'
? settings.easing
: easingPatterns[settings.easing!]
/* istanbul ignore else */
if (!ease) throw new TypeError(`Easing function "${settings.easing}" not found.`)
// Cannot be tested properly in jsdom
// tslint:disable-next-line:promise-must-complete
/* istanbul ignore next */
return new Promise(resolve => requestAnimationFrame(function step (currentTime: number) {
const timeElapsed = currentTime - startTime
const progress = Math.abs(settings.duration ? Math.min(timeElapsed / settings.duration, 1) : 1)
container.scrollTop = Math.floor(startLocation + (targetLocation - startLocation) * ease(progress))
const clientHeight = container === document.body ? document.documentElement.clientHeight : container.clientHeight
if (progress === 1 || clientHeight + container.scrollTop === container.scrollHeight) {
return resolve(targetLocation)
}
requestAnimationFrame(step)
}))
}
goTo.framework = {} as Record<string, VuetifyServiceContract>
goTo.init = () => {}
export class Goto extends Service {
public static property: 'goTo' = 'goTo'
constructor () {
super()
return goTo
}
}
+49
View File
@@ -0,0 +1,49 @@
import Vue from 'vue'
// Return target's cumulative offset from the top
export function getOffset (target: any): number {
if (typeof target === 'number') {
return target
}
let el = $(target)
if (!el) {
throw typeof target === 'string'
? new Error(`Target element "${target}" not found.`)
: new TypeError(`Target must be a Number/Selector/HTMLElement/VueComponent, received ${type(target)} instead.`)
}
let totalOffset = 0
while (el) {
totalOffset += el.offsetTop
el = el.offsetParent as HTMLElement
}
return totalOffset
}
export function getContainer (container: any): HTMLElement {
const el = $(container)
if (el) return el
throw typeof container === 'string'
? new Error(`Container element "${container}" not found.`)
: new TypeError(`Container must be a Selector/HTMLElement/VueComponent, received ${type(container)} instead.`)
}
function type (el: any) {
return el == null ? el : el.constructor.name
}
function $ (el: any): HTMLElement | null {
if (typeof el === 'string') {
return document.querySelector<HTMLElement>(el)
} else if (el && el._isVue) {
return (el as Vue).$el as HTMLElement
} else if (el instanceof HTMLElement) {
return el
} else {
return null
}
}
@@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Icons.ts should generate default icons 1`] = `
Object {
"cancel": "mdi-close-circle",
"checkboxIndeterminate": "mdi-minus-box",
"checkboxOff": "mdi-checkbox-blank-outline",
"checkboxOn": "mdi-checkbox-marked",
"clear": "mdi-close",
"close": "mdi-close",
"complete": "mdi-check",
"delete": "mdi-close-circle",
"delimiter": "mdi-circle",
"dropdown": "mdi-menu-down",
"edit": "mdi-pencil",
"error": "mdi-alert",
"expand": "mdi-chevron-down",
"file": "mdi-paperclip",
"first": "mdi-page-first",
"info": "mdi-information",
"last": "mdi-page-last",
"loading": "mdi-cached",
"menu": "mdi-menu",
"minus": "mdi-minus",
"next": "mdi-chevron-right",
"plus": "mdi-plus",
"prev": "mdi-chevron-left",
"radioOff": "mdi-radiobox-blank",
"radioOn": "mdi-radiobox-marked",
"ratingEmpty": "mdi-star-outline",
"ratingFull": "mdi-star",
"ratingHalf": "mdi-star-half",
"sort": "mdi-arrow-up",
"subgroup": "mdi-menu-down",
"success": "mdi-check-circle",
"unfold": "mdi-unfold-more-horizontal",
"warning": "mdi-exclamation",
}
`;
exports[`Icons.ts should use a custom iconfont preset 1`] = `
Object {
"cancel": "fa fa-times-circle",
"checkboxIndeterminate": "fa fa-minus-square",
"checkboxOff": "fa fa-square-o",
"checkboxOn": "fa fa-check-square",
"clear": "fa fa-times-circle",
"close": "fa fa-times",
"complete": "fa fa-check",
"delete": "fa fa-times-circle",
"delimiter": "fa fa-circle",
"dropdown": "fa fa-caret-down",
"edit": "fa fa-pencil",
"error": "fa fa-exclamation-triangle",
"expand": "fa fa-chevron-down",
"file": "fa fa-paperclip",
"first": "fa fa-step-backward",
"info": "fa fa-info-circle",
"last": "fa fa-step-forward",
"loading": "fa fa-refresh",
"menu": "fa fa-bars",
"minus": "fa fa-minus",
"next": "fa fa-chevron-right",
"plus": "fa fa-plus",
"prev": "fa fa-chevron-left",
"radioOff": "fa fa-circle-o",
"radioOn": "fa fa-dot-circle-o",
"ratingEmpty": "fa fa-star-o",
"ratingFull": "fa fa-star",
"ratingHalf": "fa fa-star-half-o",
"sort": "fa fa-sort-up",
"subgroup": "fa fa-caret-down",
"success": "fa fa-check-circle",
"unfold": "fa fa-angle-double-down",
"warning": "fa fa-exclamation",
}
`;
+31
View File
@@ -0,0 +1,31 @@
// Service
import { Icons } from '../index'
// Preset
import { preset } from '../../../presets/default'
describe('Icons.ts', () => {
let icon
it('should generate default icons', () => {
icon = new Icons(preset)
expect(icon.values).toMatchSnapshot()
})
it('should use a custom iconfont preset', () => {
preset.icons.iconfont = 'fa4'
icon = new Icons(preset)
expect(icon.values).toMatchSnapshot()
})
it('should accept custom icons', () => {
preset.icons.iconfont = 'fa4'
preset.icons.values.complete = 'fizzbuzz'
icon = new Icons(preset)
expect(icon.values.complete).toBe('fizzbuzz')
})
})
+35
View File
@@ -0,0 +1,35 @@
// Extensions
import { Service } from '../service'
// Utilities
import { mergeDeep } from '../../util/helpers'
// Types
import { VuetifyPreset } from 'vuetify/types/services/presets'
import { Icons as IIcons } from 'vuetify/types/services/icons'
// Presets
import presets from './presets'
export class Icons extends Service implements IIcons {
static property: 'icons' = 'icons'
public iconfont: IIcons['iconfont']
public values: IIcons['values']
constructor (preset: VuetifyPreset) {
super()
const {
iconfont,
values,
} = preset[Icons.property]
this.iconfont = iconfont
this.values = mergeDeep(
presets[iconfont],
values
) as IIcons['values']
}
}
+23
View File
@@ -0,0 +1,23 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
import { Component } from 'vue'
import icons from './fa'
export function convertToComponentDeclarations (
component: Component | string,
iconSet: VuetifyIcons,
) {
const result: Partial<VuetifyIcons> = {}
for (const key in iconSet) {
result[key] = {
component,
props: {
icon: (iconSet[key] as string).split(' fa-'),
},
}
}
return result as VuetifyIcons
}
export default convertToComponentDeclarations('font-awesome-icon', icons)
+39
View File
@@ -0,0 +1,39 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
const icons: VuetifyIcons = {
complete: 'fas fa-check',
cancel: 'fas fa-times-circle',
close: 'fas fa-times',
delete: 'fas fa-times-circle', // delete (e.g. v-chip close)
clear: 'fas fa-times-circle', // delete (e.g. v-chip close)
success: 'fas fa-check-circle',
info: 'fas fa-info-circle',
warning: 'fas fa-exclamation',
error: 'fas fa-exclamation-triangle',
prev: 'fas fa-chevron-left',
next: 'fas fa-chevron-right',
checkboxOn: 'fas fa-check-square',
checkboxOff: 'far fa-square', // note 'far'
checkboxIndeterminate: 'fas fa-minus-square',
delimiter: 'fas fa-circle', // for carousel
sort: 'fas fa-sort-up',
expand: 'fas fa-chevron-down',
menu: 'fas fa-bars',
subgroup: 'fas fa-caret-down',
dropdown: 'fas fa-caret-down',
radioOn: 'far fa-dot-circle',
radioOff: 'far fa-circle',
edit: 'fas fa-edit',
ratingEmpty: 'far fa-star',
ratingFull: 'fas fa-star',
ratingHalf: 'fas fa-star-half',
loading: 'fas fa-sync',
first: 'fas fa-step-backward',
last: 'fas fa-step-forward',
unfold: 'fas fa-arrows-alt-v',
file: 'fas fa-paperclip',
plus: 'fas fa-plus',
minus: 'fas fa-minus',
}
export default icons
+39
View File
@@ -0,0 +1,39 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
const icons: VuetifyIcons = {
complete: 'fa fa-check',
cancel: 'fa fa-times-circle',
close: 'fa fa-times',
delete: 'fa fa-times-circle', // delete (e.g. v-chip close)
clear: 'fa fa-times-circle', // delete (e.g. v-chip close)
success: 'fa fa-check-circle',
info: 'fa fa-info-circle',
warning: 'fa fa-exclamation',
error: 'fa fa-exclamation-triangle',
prev: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
checkboxOn: 'fa fa-check-square',
checkboxOff: 'fa fa-square-o',
checkboxIndeterminate: 'fa fa-minus-square',
delimiter: 'fa fa-circle', // for carousel
sort: 'fa fa-sort-up',
expand: 'fa fa-chevron-down',
menu: 'fa fa-bars',
subgroup: 'fa fa-caret-down',
dropdown: 'fa fa-caret-down',
radioOn: 'fa fa-dot-circle-o',
radioOff: 'fa fa-circle-o',
edit: 'fa fa-pencil',
ratingEmpty: 'fa fa-star-o',
ratingFull: 'fa fa-star',
ratingHalf: 'fa fa-star-half-o',
loading: 'fa fa-refresh',
first: 'fa fa-step-backward',
last: 'fa fa-step-forward',
unfold: 'fa fa-angle-double-down',
file: 'fa fa-paperclip',
plus: 'fa fa-plus',
minus: 'fa fa-minus',
}
export default icons
+15
View File
@@ -0,0 +1,15 @@
import mdiSvg from './mdi-svg'
import md from './md'
import mdi from './mdi'
import fa from './fa'
import fa4 from './fa4'
import faSvg from './fa-svg'
export default Object.freeze({
mdiSvg,
md,
mdi,
fa,
fa4,
faSvg,
})
+39
View File
@@ -0,0 +1,39 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
const icons: VuetifyIcons = {
complete: 'check',
cancel: 'cancel',
close: 'close',
delete: 'cancel', // delete (e.g. v-chip close)
clear: 'clear',
success: 'check_circle',
info: 'info',
warning: 'priority_high',
error: 'warning',
prev: 'chevron_left',
next: 'chevron_right',
checkboxOn: 'check_box',
checkboxOff: 'check_box_outline_blank',
checkboxIndeterminate: 'indeterminate_check_box',
delimiter: 'fiber_manual_record', // for carousel
sort: 'arrow_upward',
expand: 'keyboard_arrow_down',
menu: 'menu',
subgroup: 'arrow_drop_down',
dropdown: 'arrow_drop_down',
radioOn: 'radio_button_checked',
radioOff: 'radio_button_unchecked',
edit: 'edit',
ratingEmpty: 'star_border',
ratingFull: 'star',
ratingHalf: 'star_half',
loading: 'cached',
first: 'first_page',
last: 'last_page',
unfold: 'unfold_more',
file: 'attach_file',
plus: 'add',
minus: 'remove',
}
export default icons
+39
View File
@@ -0,0 +1,39 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
const icons: VuetifyIcons = {
complete: 'M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z',
cancel: 'M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z',
close: 'M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z',
delete: 'M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z', // delete (e.g. v-chip close)
clear: 'M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z',
success: 'M12,2C17.52,2 22,6.48 22,12C22,17.52 17.52,22 12,22C6.48,22 2,17.52 2,12C2,6.48 6.48,2 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z',
info: 'M13,9H11V7H13M13,17H11V11H13M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2Z',
warning: 'M11,4.5H13V15.5H11V4.5M13,17.5V19.5H11V17.5H13Z',
error: 'M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z',
prev: 'M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z',
next: 'M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z',
checkboxOn: 'M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M19,3H5C3.89,3 3,3.89 3,5V19C3,20.1 3.9,21 5,21H19C20.1,21 21,20.1 21,19V5C21,3.89 20.1,3 19,3Z',
checkboxOff: 'M19,3H5C3.89,3 3,3.89 3,5V19C3,20.1 3.9,21 5,21H19C20.1,21 21,20.1 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z',
checkboxIndeterminate: 'M17,13H7V11H17M19,3H5C3.89,3 3,3.89 3,5V19C3,20.1 3.9,21 5,21H19C20.1,21 21,20.1 21,19V5C21,3.89 20.1,3 19,3Z',
delimiter: 'M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2Z', // for carousel
sort: 'M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z',
expand: 'M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z',
menu: 'M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z',
subgroup: 'M7,10L12,15L17,10H7Z',
dropdown: 'M7,10L12,15L17,10H7Z',
radioOn: 'M12,20C7.58,20 4,16.42 4,12C4,7.58 7.58,4 12,4C16.42,4 20,7.58 20,12C20,16.42 16.42,20 12,20M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M12,7C9.24,7 7,9.24 7,12C7,14.76 9.24,17 12,17C14.76,17 17,14.76 17,12C17,9.24 14.76,7 12,7Z',
radioOff: 'M12,20C7.58,20 4,16.42 4,12C4,7.58 7.58,4 12,4C16.42,4 20,7.58 20,12C20,16.42 16.42,20 12,20M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2Z',
edit: 'M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z',
ratingEmpty: 'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',
ratingFull: 'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z',
ratingHalf: 'M12,15.4V6.1L13.71,10.13L18.09,10.5L14.77,13.39L15.76,17.67M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',
loading: 'M19,8L15,12H18C18,15.31 15.31,18 12,18C11,18 10.03,17.75 9.2,17.3L7.74,18.76C8.97,19.54 10.43,20 12,20C16.42,20 20,16.42 20,12H23M6,12C6,8.69 8.69,6 12,6C13,6 13.97,6.25 14.8,6.7L16.26,5.24C15.03,4.46 13.57,4 12,4C7.58,4 4,7.58 4,12H1L5,16L9,12',
first: 'M18.41,16.59L13.82,12L18.41,7.41L17,6L11,12L17,18L18.41,16.59M6,6H8V18H6V6Z',
last: 'M5.59,7.41L10.18,12L5.59,16.59L7,18L13,12L7,6L5.59,7.41M16,6H18V18H16V6Z',
unfold: 'M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z',
file: 'M16.5,6V17.5C16.5,19.71 14.71,21.5 12.5,21.5C10.29,21.5 8.5,19.71 8.5,17.5V5C8.5,3.62 9.62,2.5 11,2.5C12.38,2.5 13.5,3.62 13.5,5V15.5C13.5,16.05 13.05,16.5 12.5,16.5C11.95,16.5 11.5,16.05 11.5,15.5V6H10V15.5C10,16.88 11.12,18 12.5,18C13.88,18 15,16.88 15,15.5V5C15,2.79 13.21,1 11,1C8.79,1 7,2.79 7,5V17.5C7,20.54 9.46,23 12.5,23C15.54,23 18,20.54 18,17.5V6H16.5Z',
plus: 'M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z',
minus: 'M19,13H5V11H19V13Z',
}
export default icons
+39
View File
@@ -0,0 +1,39 @@
import { VuetifyIcons } from 'vuetify/types/services/icons'
const icons: VuetifyIcons = {
complete: 'mdi-check',
cancel: 'mdi-close-circle',
close: 'mdi-close',
delete: 'mdi-close-circle', // delete (e.g. v-chip close)
clear: 'mdi-close',
success: 'mdi-check-circle',
info: 'mdi-information',
warning: 'mdi-exclamation',
error: 'mdi-alert',
prev: 'mdi-chevron-left',
next: 'mdi-chevron-right',
checkboxOn: 'mdi-checkbox-marked',
checkboxOff: 'mdi-checkbox-blank-outline',
checkboxIndeterminate: 'mdi-minus-box',
delimiter: 'mdi-circle', // for carousel
sort: 'mdi-arrow-up',
expand: 'mdi-chevron-down',
menu: 'mdi-menu',
subgroup: 'mdi-menu-down',
dropdown: 'mdi-menu-down',
radioOn: 'mdi-radiobox-marked',
radioOff: 'mdi-radiobox-blank',
edit: 'mdi-pencil',
ratingEmpty: 'mdi-star-outline',
ratingFull: 'mdi-star',
ratingHalf: 'mdi-star-half',
loading: 'mdi-cached',
first: 'mdi-page-first',
last: 'mdi-page-last',
unfold: 'mdi-unfold-more-horizontal',
file: 'mdi-paperclip',
plus: 'mdi-plus',
minus: 'mdi-minus',
}
export default icons
+7
View File
@@ -0,0 +1,7 @@
export * from './application'
export * from './breakpoint'
export * from './goto'
export * from './icons'
export * from './lang'
export * from './presets'
export * from './theme'
+70
View File
@@ -0,0 +1,70 @@
// Service
import { Lang } from '../index'
// Preset
import { preset } from '../../../presets/default'
describe('$vuetify.lang', () => {
let lang: Lang
beforeEach(() => {
lang = new Lang(preset)
})
it('should fall back to en', () => {
Object.assign(lang.locales.en, { foo: 'bar', bar: 'baz' })
lang.locales.foreign = { foo: 'foreignBar' }
lang.current = 'foreign'
expect(lang.t('$vuetify.foo')).toBe('foreignBar')
expect(lang.t('$vuetify.bar')).toBe('baz')
expect('Translation key "bar" not found, falling back to default').toHaveBeenTipped()
expect(lang.t('$vuetify.baz')).toBe('$vuetify.baz')
expect('Translation key "baz" not found, falling back to default').toHaveBeenTipped()
expect('Translation key "baz" not found in fallback').toHaveBeenWarned()
})
it('should ignore unprefixed strings', () => {
expect(lang.t('foo.bar.baz')).toBe('foo.bar.baz')
})
it('should use a different default', () => {
lang = new Lang({
...preset,
lang: {
current: 'foreign',
locales: {
foreign: { foo: 'foreignBar' },
},
},
})
expect(lang.t('$vuetify.foo')).toBe('foreignBar')
})
it('should use a custom translator', () => {
const translator = jest.fn(str => str)
lang = new Lang({
...preset,
lang: { t: translator },
})
lang.t('$vuetify.foobar', 'fizzbuzz')
expect(translator).toHaveBeenCalledWith('$vuetify.foobar', 'fizzbuzz')
})
it('should replace params on a non-prefixed key', () => {
lang = new Lang({
...preset,
lang: { t: str => str },
})
const translated = lang.t('{2} bar {0} foo {1}', 'hello', 'world', '!')
expect(translated).toBe('! bar hello foo world')
})
})
+88
View File
@@ -0,0 +1,88 @@
// Extensions
import { Service } from '../service'
// Utilities
import { getObjectValueByPath } from '../../util/helpers'
import { consoleError, consoleWarn } from '../../util/console'
// Types
import { VuetifyPreset } from 'vuetify/types/services/presets'
import {
VuetifyLocale,
Lang as ILang,
} from 'vuetify/types/services/lang'
const LANG_PREFIX = '$vuetify.'
const fallback = Symbol('Lang fallback')
function getTranslation (
locale: VuetifyLocale,
key: string,
usingDefault = false,
defaultLocale: VuetifyLocale
): string {
const shortKey = key.replace(LANG_PREFIX, '')
let translation = getObjectValueByPath(locale, shortKey, fallback) as string | typeof fallback
if (translation === fallback) {
if (usingDefault) {
consoleError(`Translation key "${shortKey}" not found in fallback`)
translation = key
} else {
consoleWarn(`Translation key "${shortKey}" not found, falling back to default`)
translation = getTranslation(defaultLocale, key, true, defaultLocale)
}
}
return translation
}
export class Lang extends Service implements ILang {
static property: 'lang' = 'lang'
public current: ILang['current']
public defaultLocale = 'en'
public locales: ILang['locales']
private translator: ILang['t']
constructor (preset: VuetifyPreset) {
super()
const {
current,
locales,
t,
} = preset[Lang.property]
this.current = current
this.locales = locales
this.translator = t || this.defaultTranslator
}
public currentLocale (key: string) {
const translation = this.locales[this.current]
const defaultLocale = this.locales[this.defaultLocale]
return getTranslation(translation, key, false, defaultLocale)
}
public t (key: string, ...params: any[]) {
if (!key.startsWith(LANG_PREFIX)) return this.replace(key, params)
return this.translator(key, ...params)
}
private defaultTranslator (key: string, ...params: any[]) {
return this.replace(this.currentLocale(key), params)
}
private replace (str: string, params: any[]) {
return str.replace(/\{(\d+)\}/g, (match: string, index: string) => {
/* istanbul ignore next */
return String(params[+index])
})
}
}
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`$vuetify.presets should merge user and default preset 1`] = `"{\\"breakpoint\\":{\\"mobileBreakpoint\\":1264,\\"scrollBarWidth\\":16,\\"thresholds\\":{\\"xs\\":200,\\"sm\\":960,\\"md\\":1280,\\"lg\\":1920}},\\"icons\\":{\\"iconfont\\":\\"md\\",\\"values\\":{\\"complete\\":\\"bar\\"}},\\"lang\\":{\\"current\\":\\"en\\",\\"locales\\":{\\"en\\":{\\"badge\\":\\"Foobar\\",\\"close\\":\\"Close\\",\\"dataIterator\\":{\\"noResultsText\\":\\"Fizzbuzz\\",\\"loadingText\\":\\"Loading items...\\"},\\"dataTable\\":{\\"itemsPerPageText\\":\\"Rows per page:\\",\\"ariaLabel\\":{\\"sortDescending\\":\\"Sorted descending.\\",\\"sortAscending\\":\\"Sorted ascending.\\",\\"sortNone\\":\\"Not sorted.\\",\\"activateNone\\":\\"Activate to remove sorting.\\",\\"activateDescending\\":\\"Activate to sort descending.\\",\\"activateAscending\\":\\"Activate to sort ascending.\\"},\\"sortBy\\":\\"Sort by\\"},\\"dataFooter\\":{\\"itemsPerPageText\\":\\"Items per page:\\",\\"itemsPerPageAll\\":\\"All\\",\\"nextPage\\":\\"Next page\\",\\"prevPage\\":\\"Previous page\\",\\"firstPage\\":\\"First page\\",\\"lastPage\\":\\"Last page\\",\\"pageText\\":\\"{0}-{1} of {2}\\"},\\"datePicker\\":{\\"itemsSelected\\":\\"{0} selected\\",\\"nextMonthAriaLabel\\":\\"Next month\\",\\"nextYearAriaLabel\\":\\"Next year\\",\\"prevMonthAriaLabel\\":\\"Previous month\\",\\"prevYearAriaLabel\\":\\"Previous year\\"},\\"noDataText\\":\\"No data available\\",\\"carousel\\":{\\"prev\\":\\"Previous visual\\",\\"next\\":\\"Next visual\\",\\"ariaLabel\\":{\\"delimiter\\":\\"Carousel slide {0} of {1}\\"}},\\"calendar\\":{\\"moreEvents\\":\\"{0} more\\"},\\"fileInput\\":{\\"counter\\":\\"{0} files\\",\\"counterSize\\":\\"{0} files ({1} in total)\\"},\\"timePicker\\":{\\"am\\":\\"AM\\",\\"pm\\":\\"PM\\"},\\"pagination\\":{\\"ariaLabel\\":{\\"wrapper\\":\\"Pagination Navigation\\",\\"next\\":\\"Next page\\",\\"previous\\":\\"Previous page\\",\\"page\\":\\"Goto Page {0}\\",\\"currentPage\\":\\"Current Page, Page {0}\\"}}}}},\\"rtl\\":true,\\"theme\\":{\\"dark\\":true,\\"default\\":\\"light\\",\\"disable\\":false,\\"options\\":{\\"variations\\":true},\\"themes\\":{\\"light\\":{\\"primary\\":\\"blue\\",\\"secondary\\":{\\"darken4\\":\\"red\\"},\\"accent\\":\\"#82B1FF\\",\\"error\\":\\"#FF5252\\",\\"info\\":\\"#2196F3\\",\\"success\\":\\"#4CAF50\\",\\"warning\\":\\"#FB8C00\\"},\\"dark\\":{\\"primary\\":\\"#2196F3\\",\\"secondary\\":\\"#424242\\",\\"accent\\":\\"#FF4081\\",\\"error\\":\\"#FF5252\\",\\"info\\":\\"#2196F3\\",\\"success\\":\\"#4CAF50\\",\\"warning\\":\\"#FB8C00\\"}}}}"`;
exports[`$vuetify.presets should merge user and default preset 2`] = `undefined`;
exports[`$vuetify.presets should merge user and default preset 3`] = `"{\\"en\\":{\\"badge\\":\\"Foobar\\",\\"close\\":\\"Close\\",\\"dataIterator\\":{\\"noResultsText\\":\\"Fizzbuzz\\",\\"loadingText\\":\\"Loading items...\\"},\\"dataTable\\":{\\"itemsPerPageText\\":\\"Rows per page:\\",\\"ariaLabel\\":{\\"sortDescending\\":\\"Sorted descending.\\",\\"sortAscending\\":\\"Sorted ascending.\\",\\"sortNone\\":\\"Not sorted.\\",\\"activateNone\\":\\"Activate to remove sorting.\\",\\"activateDescending\\":\\"Activate to sort descending.\\",\\"activateAscending\\":\\"Activate to sort ascending.\\"},\\"sortBy\\":\\"Sort by\\"},\\"dataFooter\\":{\\"itemsPerPageText\\":\\"Items per page:\\",\\"itemsPerPageAll\\":\\"All\\",\\"nextPage\\":\\"Next page\\",\\"prevPage\\":\\"Previous page\\",\\"firstPage\\":\\"First page\\",\\"lastPage\\":\\"Last page\\",\\"pageText\\":\\"{0}-{1} of {2}\\"},\\"datePicker\\":{\\"itemsSelected\\":\\"{0} selected\\",\\"nextMonthAriaLabel\\":\\"Next month\\",\\"nextYearAriaLabel\\":\\"Next year\\",\\"prevMonthAriaLabel\\":\\"Previous month\\",\\"prevYearAriaLabel\\":\\"Previous year\\"},\\"noDataText\\":\\"No data available\\",\\"carousel\\":{\\"prev\\":\\"Previous visual\\",\\"next\\":\\"Next visual\\",\\"ariaLabel\\":{\\"delimiter\\":\\"Carousel slide {0} of {1}\\"}},\\"calendar\\":{\\"moreEvents\\":\\"{0} more\\"},\\"fileInput\\":{\\"counter\\":\\"{0} files\\",\\"counterSize\\":\\"{0} files ({1} in total)\\"},\\"timePicker\\":{\\"am\\":\\"AM\\",\\"pm\\":\\"PM\\"},\\"pagination\\":{\\"ariaLabel\\":{\\"wrapper\\":\\"Pagination Navigation\\",\\"next\\":\\"Next page\\",\\"previous\\":\\"Previous page\\",\\"page\\":\\"Goto Page {0}\\",\\"currentPage\\":\\"Current Page, Page {0}\\"}}}}"`;
exports[`$vuetify.presets should merge user and default preset 4`] = `"{\\"dark\\":{\\"primary\\":\\"#2196F3\\",\\"secondary\\":\\"#424242\\",\\"accent\\":\\"#FF4081\\",\\"error\\":\\"#FF5252\\",\\"info\\":\\"#2196F3\\",\\"success\\":\\"#4CAF50\\",\\"warning\\":\\"#FB8C00\\"},\\"light\\":{\\"primary\\":\\"blue\\",\\"secondary\\":{\\"darken4\\":\\"red\\"},\\"accent\\":\\"#82B1FF\\",\\"error\\":\\"#FF5252\\",\\"info\\":\\"#2196F3\\",\\"success\\":\\"#4CAF50\\",\\"warning\\":\\"#FB8C00\\"}}"`;
exports[`$vuetify.presets should merge user and default preset 5`] = `"{\\"xs\\":200,\\"sm\\":960,\\"md\\":1280,\\"lg\\":1920}"`;
+51
View File
@@ -0,0 +1,51 @@
// Types
import Framework from '../../../framework'
import { Breakpoint } from 'vuetify/types/services/breakpoint'
import { Icons } from 'vuetify/types/services/icons'
import { Lang } from 'vuetify/types/services/lang'
import { Theme } from 'vuetify/types/services/theme'
describe('$vuetify.presets', () => {
it('should merge user and default preset', () => {
const vuetify = new Framework({
rtl: true,
breakpoint: {
thresholds: { xs: 200 },
},
icons: {
iconfont: 'md',
values: { complete: 'bar' },
},
lang: {
locales: {
en: {
badge: 'Foobar',
dataIterator: { noResultsText: 'Fizzbuzz' },
},
},
},
theme: {
dark: true,
themes: {
light: {
primary: 'blue',
// https://github.com/vuetifyjs/vuetify/issues/10100
secondary: { darken4: 'red' },
},
},
},
})
expect(JSON.stringify(vuetify.preset)).toMatchSnapshot()
const icons: Icons = vuetify.framework as any
const lang: Lang = vuetify.framework.lang as any
const itheme: Theme = vuetify.framework.theme as any
const breakpoints: Breakpoint = vuetify.framework.breakpoint as any
expect(JSON.stringify(icons.values)).toMatchSnapshot()
expect(JSON.stringify(lang.locales)).toMatchSnapshot()
expect(JSON.stringify(itheme.themes)).toMatchSnapshot()
expect(JSON.stringify(breakpoints.thresholds)).toMatchSnapshot()
})
})
+44
View File
@@ -0,0 +1,44 @@
// Preset
import { preset as Preset } from '../../presets/default'
// Utilities
import { consoleWarn } from '../../util/console'
import { mergeDeep } from '../../util/helpers'
// Types
import Framework from 'vuetify/types'
import { Service } from '../service'
import {
UserVuetifyPreset,
VuetifyPreset,
} from 'vuetify/types/services/presets'
export class Presets extends Service {
static property: 'presets' = 'presets'
constructor (
parentPreset: Partial<UserVuetifyPreset>,
parent: InstanceType<typeof Framework>,
) {
super()
// The default preset
const defaultPreset = mergeDeep({}, Preset)
// The user provided preset
const { userPreset } = parent
// The user provided global preset
const {
preset: globalPreset = {},
...preset
} = userPreset
if (globalPreset.preset != null) {
consoleWarn('Global presets do not support the **preset** option, it can be safely omitted')
}
parent.preset = mergeDeep(
mergeDeep(defaultPreset, globalPreset),
preset
) as VuetifyPreset
}
}
+11
View File
@@ -0,0 +1,11 @@
// Contracts
import { VuetifyServiceContract } from 'vuetify/types/services/index'
// Types
import Vue from 'vue'
export class Service implements VuetifyServiceContract {
framework = {}
init (root: Vue, ssrContext?: object) {}
}
@@ -0,0 +1,231 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`theme-utilities.ts should generate css vars 1`] = `
":root {
--v-anchor-base: #c42742;
--v-primary-base: #c42742;
--v-primary-lighten5: #2c0447;
--v-primary-lighten4: #cfa854;
--v-primary-lighten3: #dd88cc;
--v-primary-lighten2: #b49921;
--v-primary-lighten1: #f899c7;
--v-primary-darken1: #0169dc;
--v-primary-darken2: #c28fd0;
--v-primary-darken3: #fbe002;
--v-primary-darken4: #33303a;
}
.v-application a { color: var(--v-anchor-base); }
.v-application .primary {
background-color: var(--v-primary-base) !important;
border-color: var(--v-primary-base) !important;
}
.v-application .primary--text {
color: var(--v-primary-base) !important;
caret-color: var(--v-primary-base) !important;
}
.v-application .primary.lighten-5 {
background-color: var(--v-primary-lighten5) !important;
border-color: var(--v-primary-lighten5) !important;
}
.v-application .primary--text.text--lighten-5 {
color: var(--v-primary-lighten5) !important;
caret-color: var(--v-primary-lighten5) !important;
}
.v-application .primary.lighten-4 {
background-color: var(--v-primary-lighten4) !important;
border-color: var(--v-primary-lighten4) !important;
}
.v-application .primary--text.text--lighten-4 {
color: var(--v-primary-lighten4) !important;
caret-color: var(--v-primary-lighten4) !important;
}
.v-application .primary.lighten-3 {
background-color: var(--v-primary-lighten3) !important;
border-color: var(--v-primary-lighten3) !important;
}
.v-application .primary--text.text--lighten-3 {
color: var(--v-primary-lighten3) !important;
caret-color: var(--v-primary-lighten3) !important;
}
.v-application .primary.lighten-2 {
background-color: var(--v-primary-lighten2) !important;
border-color: var(--v-primary-lighten2) !important;
}
.v-application .primary--text.text--lighten-2 {
color: var(--v-primary-lighten2) !important;
caret-color: var(--v-primary-lighten2) !important;
}
.v-application .primary.lighten-1 {
background-color: var(--v-primary-lighten1) !important;
border-color: var(--v-primary-lighten1) !important;
}
.v-application .primary--text.text--lighten-1 {
color: var(--v-primary-lighten1) !important;
caret-color: var(--v-primary-lighten1) !important;
}
.v-application .primary.darken-1 {
background-color: var(--v-primary-darken1) !important;
border-color: var(--v-primary-darken1) !important;
}
.v-application .primary--text.text--darken-1 {
color: var(--v-primary-darken1) !important;
caret-color: var(--v-primary-darken1) !important;
}
.v-application .primary.darken-2 {
background-color: var(--v-primary-darken2) !important;
border-color: var(--v-primary-darken2) !important;
}
.v-application .primary--text.text--darken-2 {
color: var(--v-primary-darken2) !important;
caret-color: var(--v-primary-darken2) !important;
}
.v-application .primary.darken-3 {
background-color: var(--v-primary-darken3) !important;
border-color: var(--v-primary-darken3) !important;
}
.v-application .primary--text.text--darken-3 {
color: var(--v-primary-darken3) !important;
caret-color: var(--v-primary-darken3) !important;
}
.v-application .primary.darken-4 {
background-color: var(--v-primary-darken4) !important;
border-color: var(--v-primary-darken4) !important;
}
.v-application .primary--text.text--darken-4 {
color: var(--v-primary-darken4) !important;
caret-color: var(--v-primary-darken4) !important;
}"
`;
exports[`theme-utilities.ts should generate styles 1`] = `
".v-application a { color: #c42742; }
.v-application .primary {
background-color: #c42742 !important;
border-color: #c42742 !important;
}
.v-application .primary--text {
color: #c42742 !important;
caret-color: #c42742 !important;
}
.v-application .primary.lighten-5 {
background-color: #2c0447 !important;
border-color: #2c0447 !important;
}
.v-application .primary--text.text--lighten-5 {
color: #2c0447 !important;
caret-color: #2c0447 !important;
}
.v-application .primary.lighten-4 {
background-color: #cfa854 !important;
border-color: #cfa854 !important;
}
.v-application .primary--text.text--lighten-4 {
color: #cfa854 !important;
caret-color: #cfa854 !important;
}
.v-application .primary.lighten-3 {
background-color: #dd88cc !important;
border-color: #dd88cc !important;
}
.v-application .primary--text.text--lighten-3 {
color: #dd88cc !important;
caret-color: #dd88cc !important;
}
.v-application .primary.lighten-2 {
background-color: #b49921 !important;
border-color: #b49921 !important;
}
.v-application .primary--text.text--lighten-2 {
color: #b49921 !important;
caret-color: #b49921 !important;
}
.v-application .primary.lighten-1 {
background-color: #f899c7 !important;
border-color: #f899c7 !important;
}
.v-application .primary--text.text--lighten-1 {
color: #f899c7 !important;
caret-color: #f899c7 !important;
}
.v-application .primary.darken-1 {
background-color: #0169dc !important;
border-color: #0169dc !important;
}
.v-application .primary--text.text--darken-1 {
color: #0169dc !important;
caret-color: #0169dc !important;
}
.v-application .primary.darken-2 {
background-color: #c28fd0 !important;
border-color: #c28fd0 !important;
}
.v-application .primary--text.text--darken-2 {
color: #c28fd0 !important;
caret-color: #c28fd0 !important;
}
.v-application .primary.darken-3 {
background-color: #fbe002 !important;
border-color: #fbe002 !important;
}
.v-application .primary--text.text--darken-3 {
color: #fbe002 !important;
caret-color: #fbe002 !important;
}
.v-application .primary.darken-4 {
background-color: #33303a !important;
border-color: #33303a !important;
}
.v-application .primary--text.text--darken-4 {
color: #33303a !important;
caret-color: #33303a !important;
}"
`;
exports[`theme-utilities.ts should parse a theme or theme item 1`] = `
Object {
"anchor": "#000000",
"primary": Object {
"base": "#000000",
"darken1": "#000000",
"darken2": "#000000",
"darken3": "#000000",
"darken4": "#000000",
"lighten1": "#1b1b1b",
"lighten2": "#303030",
"lighten3": "#474747",
"lighten4": "#5e5e5e",
"lighten5": "#777777",
},
"secondary": Object {
"base": "#ffffff",
"darken1": "#e2e2e2",
"darken2": "#c6c6c6",
"darken3": "#ababab",
"darken4": "#919191",
"lighten1": "#ffffff",
"lighten2": "#ffffff",
"lighten3": "#ffffff",
"lighten4": "#ffffff",
"lighten5": "#ffffff",
},
}
`;
exports[`theme-utilities.ts should parse a theme or theme item 2`] = `
Object {
"anchor": "#c42742",
"primary": Object {
"base": "#c42742",
"darken1": "#0169dc",
"darken2": "#c28fd0",
"darken3": "#fbe002",
"darken4": "#33303a",
"lighten1": "#f899c7",
"lighten2": "#b49921",
"lighten3": "#dd88cc",
"lighten4": "#cfa854",
"lighten5": "#2c0447",
},
}
`;
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
import {
genStyles,
parse,
} from '../utils'
describe('theme-utilities.ts', () => {
let parsedTheme
beforeEach(() => {
parsedTheme = {
primary: {
base: '#c42742',
lighten5: '#2c0447',
lighten4: '#cfa854',
lighten3: '#dd88cc',
lighten2: '#b49921',
lighten1: '#f899c7',
darken1: '#0169dc',
darken2: '#c28fd0',
darken3: '#fbe002',
darken4: '#33303a',
},
anchor: '#c42742',
}
})
it('should parse a theme or theme item', () => {
const theme = {
primary: '#000',
secondary: '#fff',
}
expect(parse(theme)).toMatchSnapshot()
expect(parse(parsedTheme)).toMatchSnapshot()
})
it('should generate styles', () => {
// No values provided
expect(genStyles({})).toBe('')
expect(genStyles(parsedTheme)).toMatchSnapshot()
})
it('should generate css vars', () => {
expect(genStyles(parsedTheme, true)).toMatchSnapshot()
})
it('should use custom anchor color', () => {
// Uses primary base as fallback
expect(genStyles(parsedTheme)).toContain('a { color: #c42742; }')
parsedTheme.anchor = '#000000'
expect(genStyles(parsedTheme)).toContain('a { color: #000000; }')
})
})
+299
View File
@@ -0,0 +1,299 @@
// Service
import { Theme } from '../index'
// Preset
import { preset } from '../../../presets/default'
// Utilities
import { mergeDeep } from '../../../util/helpers'
// Types
import Vue from 'vue'
import {
VuetifyParsedTheme,
VuetifyThemeVariant,
ThemeOptions,
} from 'vuetify/types/services/theme'
const FillVariant = (variant: Partial<VuetifyThemeVariant> = {}) => {
return {
primary: '#1976D2',
secondary: '#424242',
accent: '#82B1FF',
error: '#FF5252',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107',
...variant,
}
}
describe('Theme.ts', () => {
function rootFactory () {
return mergeDeep(JSON.parse(JSON.stringify(preset)), {
theme: {
default: 'light',
themes: {
dark: FillVariant(),
light: FillVariant(),
},
},
})
}
let mockTheme: (theme?: Partial<ThemeOptions>) => Theme
let instance: Vue
beforeEach(() => {
mockTheme = (theme?: Partial<ThemeOptions>) => {
const options = { theme: theme || {} }
return new Theme(mergeDeep(rootFactory(), options))
}
instance = new Vue()
})
afterEach(() => {
const style = document.getElementById('vuetify-theme-stylesheet')
style && style.remove()
})
it('should disable theme colors', () => {
const theme = mockTheme({ disable: true })
theme.init(instance)
expect(theme.styleEl).toBeFalsy()
})
it('should generate theme and apply to document', () => {
const theme = mockTheme({
themes: {
light: FillVariant({
primary: '#000001',
secondary: '#000002',
accent: '#000003',
}),
},
})
theme.init(instance)
const style = document.getElementById('vuetify-theme-stylesheet')
const html = style!.innerHTML
expect(html).toMatchSnapshot()
expect(html.indexOf('#000001') > -1).toBe(true)
expect(html.indexOf('#000002') > -1).toBe(true)
expect(html.indexOf('#000003') > -1).toBe(true)
})
it('should apply a new theme', () => {
const theme = mockTheme({
default: 'light',
themes: {
light: FillVariant(),
dark: FillVariant({
primary: '#FFFFFF',
}),
},
})
theme.init(instance)
const style = document.getElementById('vuetify-theme-stylesheet')
const html = style!.innerHTML
theme.dark = true
expect(html).not.toEqual(style!.innerHTML)
})
it('should clear css', () => {
const theme = mockTheme()
const spy = jest.spyOn(theme, 'clearCss')
theme.dark = true
expect(spy).toHaveBeenCalledTimes(0)
theme.themes.light = FillVariant()
theme.dark = false
expect(spy).toHaveBeenCalledTimes(0)
theme.disabled = true
theme.dark = true
expect(spy).toHaveBeenCalledTimes(1)
})
it('should use themeCache', () => {
let cache: VuetifyParsedTheme | undefined
const themeCache = {
get: jest.fn(() => cache),
set: jest.fn((obj: VuetifyParsedTheme) => {
cache = obj
}),
}
const theme = mockTheme({
options: { themeCache },
})
expect(theme.generatedStyles).toMatchSnapshot()
expect(themeCache.set).toHaveBeenCalledTimes(1)
theme.applyTheme()
expect(themeCache.get).toHaveBeenCalledTimes(2)
expect(themeCache.set).toHaveBeenCalledTimes(1)
expect(theme.generatedStyles).toMatchSnapshot()
})
it('should minify theme', () => {
const minifyTheme = jest.fn((css: string) => css + 'foobar')
const theme = mockTheme({
options: { minifyTheme },
})
theme.init(instance)
const style = document.getElementById('vuetify-theme-stylesheet')
const html = style!.innerHTML
expect(minifyTheme).toHaveBeenCalled()
expect(html.indexOf('foobar') > -1).toBe(true)
expect(theme.generatedStyles).toMatchSnapshot()
})
it('should add nonce to stylesheet', () => {
const theme = mockTheme({
options: { cspNonce: 'foobar' },
})
theme.init(instance)
const style = document.getElementById('vuetify-theme-stylesheet')
expect(style!.getAttribute('nonce')).toBe('foobar')
})
it('should initialize the theme', () => {
const theme = mockTheme()
const spy = jest.spyOn(theme, 'applyTheme')
const ssrContext = { head: '' }
theme.init(instance, ssrContext)
expect(spy).toHaveBeenCalledTimes(1)
expect(ssrContext.head).toBeTruthy()
expect(ssrContext.head).toMatchSnapshot()
})
it('should set theme with vue-meta@1', () => {
const theme = mockTheme()
const anyInstance = instance as any
anyInstance.$meta = () => ({})
theme.init(anyInstance)
expect(typeof anyInstance.$options['metaInfo']).toBe('function')
const metaInfo = anyInstance.$options['metaInfo']()
expect(metaInfo).toBeTruthy()
expect(metaInfo.style).toHaveLength(1)
expect(metaInfo.style[0].cssText).toMatchSnapshot()
})
it('should set theme with vue-meta@2', () => {
const theme = mockTheme()
const anyInstance = instance as any
anyInstance.$meta = () => ({
getOptions: () => ({ keyName: 'metaInfo' }),
})
theme.init(anyInstance)
const metaKeyName = anyInstance.$meta().getOptions().keyName
expect(typeof anyInstance.$options[metaKeyName]).toBe('function')
const metaInfo = anyInstance.$options[metaKeyName]()
expect(metaInfo).toBeTruthy()
expect(metaInfo.style).toHaveLength(1)
expect(metaInfo.style[0].cssText).toMatchSnapshot()
})
it('should react to theme changes', async () => {
const theme = mockTheme()
const spy = jest.spyOn(theme, 'applyTheme')
theme.init(instance)
expect(spy).toHaveBeenCalledTimes(1)
theme.themes.light.primary = '#000000'
await instance.$nextTick()
theme.themes.dark.secondary = '#000000'
await instance.$nextTick()
theme.currentTheme.accent = '#000000'
await instance.$nextTick()
expect(spy).toHaveBeenCalledTimes(4)
})
it('should reset themes', async () => {
const theme = mockTheme()
const spy = jest.spyOn(theme, 'applyTheme')
theme.init(instance)
expect(theme.generatedStyles).toMatchSnapshot()
theme.resetThemes()
expect(theme.generatedStyles).toMatchSnapshot()
expect(spy).toHaveBeenCalledTimes(2)
})
it('should set theme', () => {
const theme = mockTheme()
const spy = jest.spyOn(theme, 'applyTheme')
theme.init(instance)
expect(theme.generatedStyles).toMatchSnapshot()
theme.setTheme('light', { accent: '#c0ffee' })
expect(theme.generatedStyles).toMatchSnapshot()
theme.setTheme('dark', { accent: '#c0ffee' })
expect(theme.generatedStyles).toMatchSnapshot()
expect(spy).toHaveBeenCalledTimes(3)
})
it('should use vue-meta@2.3 functionality', () => {
const theme = mockTheme()
const set = jest.fn()
const $meta = () => ({
addApp: () => ({ set }),
})
;(instance as any).$meta = $meta as any
theme.init(instance)
expect(set).toHaveBeenCalled()
})
it('should not generate variations', () => {
const theme = mockTheme({ options: { variations: false } })
theme.init(instance)
const style = document.getElementById('vuetify-theme-stylesheet')
const html = style!.innerHTML
expect(html).toMatchSnapshot()
})
})
+293
View File
@@ -0,0 +1,293 @@
/* eslint-disable no-multi-spaces */
// Extensions
import { Service } from '../service'
// Utilities
import * as ThemeUtils from './utils'
import { getNestedValue } from '../../util/helpers'
// Types
import Vue from 'vue'
import { VuetifyPreset } from 'vuetify/types/services/presets'
import {
VuetifyParsedTheme,
VuetifyThemes,
VuetifyThemeVariant,
Theme as ITheme,
} from 'vuetify/types/services/theme'
export class Theme extends Service {
static property: 'theme' = 'theme'
public disabled = false
public options: ITheme['options']
public styleEl?: HTMLStyleElement
public themes: VuetifyThemes
public defaults: VuetifyThemes
private isDark = null as boolean | null
private vueInstance = null as Vue | null
private vueMeta = null as any | null
constructor (preset: VuetifyPreset) {
super()
const {
dark,
disable,
options,
themes,
} = preset[Theme.property]
this.dark = Boolean(dark)
this.defaults = this.themes = themes
this.options = options
if (disable) {
this.disabled = true
return
}
this.themes = {
dark: this.fillVariant(themes.dark, true),
light: this.fillVariant(themes.light, false),
}
}
// When setting css, check for element
// and apply new values
set css (val: string) {
if (this.vueMeta) {
if (this.isVueMeta23) {
this.applyVueMeta23()
}
return
}
this.checkOrCreateStyleElement() && (this.styleEl!.innerHTML = val)
}
set dark (val: boolean) {
const oldDark = this.isDark
this.isDark = val
// Only apply theme after dark
// has already been set before
oldDark != null && this.applyTheme()
}
get dark () {
return Boolean(this.isDark)
}
// Apply current theme default
// only called on client side
public applyTheme (): void {
if (this.disabled) return this.clearCss()
this.css = this.generatedStyles
}
public clearCss (): void {
this.css = ''
}
// Initialize theme for SSR and SPA
// Attach to ssrContext head or
// apply new theme to document
public init (root: Vue, ssrContext?: any): void {
if (this.disabled) return
/* istanbul ignore else */
if ((root as any).$meta) {
this.initVueMeta(root)
} else if (ssrContext) {
this.initSSR(ssrContext)
}
this.initTheme()
}
// Allows for you to set target theme
public setTheme (theme: 'light' | 'dark', value: object) {
this.themes[theme] = Object.assign(this.themes[theme], value)
this.applyTheme()
}
// Reset theme defaults
public resetThemes () {
this.themes.light = Object.assign({}, this.defaults.light)
this.themes.dark = Object.assign({}, this.defaults.dark)
this.applyTheme()
}
// Check for existence of style element
private checkOrCreateStyleElement (): boolean {
this.styleEl = document.getElementById('vuetify-theme-stylesheet') as HTMLStyleElement
/* istanbul ignore next */
if (this.styleEl) return true
this.genStyleElement() // If doesn't have it, create it
return Boolean(this.styleEl)
}
private fillVariant (
theme: Partial<VuetifyThemeVariant> = {},
dark: boolean
): VuetifyThemeVariant {
const defaultTheme = this.themes[dark ? 'dark' : 'light']
return Object.assign({},
defaultTheme,
theme
)
}
// Generate the style element
// if applicable
private genStyleElement (): void {
/* istanbul ignore if */
if (typeof document === 'undefined') return
/* istanbul ignore next */
this.styleEl = document.createElement('style')
this.styleEl.type = 'text/css'
this.styleEl.id = 'vuetify-theme-stylesheet'
if (this.options.cspNonce) {
this.styleEl.setAttribute('nonce', this.options.cspNonce)
}
document.head.appendChild(this.styleEl)
}
private initVueMeta (root: any) {
this.vueMeta = root.$meta()
if (this.isVueMeta23) {
// vue-meta needs to apply after mounted()
root.$nextTick(() => {
this.applyVueMeta23()
})
return
}
const metaKeyName = typeof this.vueMeta.getOptions === 'function' ? this.vueMeta.getOptions().keyName : 'metaInfo'
const metaInfo = root.$options[metaKeyName] || {}
root.$options[metaKeyName] = () => {
metaInfo.style = metaInfo.style || []
const vuetifyStylesheet = metaInfo.style.find((s: any) => s.id === 'vuetify-theme-stylesheet')
if (!vuetifyStylesheet) {
metaInfo.style.push({
cssText: this.generatedStyles,
type: 'text/css',
id: 'vuetify-theme-stylesheet',
nonce: (this.options || {}).cspNonce,
})
} else {
vuetifyStylesheet.cssText = this.generatedStyles
}
return metaInfo
}
}
private applyVueMeta23 () {
const { set } = this.vueMeta.addApp('vuetify')
set({
style: [{
cssText: this.generatedStyles,
type: 'text/css',
id: 'vuetify-theme-stylesheet',
nonce: this.options.cspNonce,
}],
})
}
private initSSR (ssrContext?: any) {
// SSR
const nonce = this.options.cspNonce ? ` nonce="${this.options.cspNonce}"` : ''
ssrContext.head = ssrContext.head || ''
ssrContext.head += `<style type="text/css" id="vuetify-theme-stylesheet"${nonce}>${this.generatedStyles}</style>`
}
private initTheme () {
// Only watch for reactivity on client side
if (typeof document === 'undefined') return
// If we get here somehow, ensure
// existing instance is removed
if (this.vueInstance) this.vueInstance.$destroy()
// Use Vue instance to track reactivity
// TODO: Update to use RFC if merged
// https://github.com/vuejs/rfcs/blob/advanced-reactivity-api/active-rfcs/0000-advanced-reactivity-api.md
this.vueInstance = new Vue({
data: { themes: this.themes },
watch: {
themes: {
immediate: true,
deep: true,
handler: () => this.applyTheme(),
},
},
})
}
get currentTheme () {
const target = this.dark ? 'dark' : 'light'
return this.themes[target]
}
get generatedStyles (): string {
const theme = this.parsedTheme
/* istanbul ignore next */
const options = this.options || {}
let css
if (options.themeCache != null) {
css = options.themeCache.get(theme)
/* istanbul ignore if */
if (css != null) return css
}
css = ThemeUtils.genStyles(theme, options.customProperties)
if (options.minifyTheme != null) {
css = options.minifyTheme(css)
}
if (options.themeCache != null) {
options.themeCache.set(theme, css)
}
return css
}
get parsedTheme (): VuetifyParsedTheme {
return ThemeUtils.parse(
this.currentTheme || {},
undefined,
getNestedValue(this.options, ['variations'], true)
)
}
// Is using v2.3 of vue-meta
// https://github.com/nuxt/vue-meta/releases/tag/v2.3.0
private get isVueMeta23 (): boolean {
return typeof this.vueMeta.addApp === 'function'
}
}
+144
View File
@@ -0,0 +1,144 @@
import { colorToInt, intToHex, colorToHex, ColorInt } from '../../util/colorUtils'
import * as sRGB from '../../util/color/transformSRGB'
import * as LAB from '../../util/color/transformCIELAB'
import {
VuetifyParsedTheme,
VuetifyThemeItem,
} from 'vuetify/types/services/theme'
export function parse (
theme: Record<string, VuetifyThemeItem>,
isItem = false,
variations = true,
): VuetifyParsedTheme {
const { anchor, ...variant } = theme
const colors = Object.keys(variant)
const parsedTheme: any = {}
for (let i = 0; i < colors.length; ++i) {
const name = colors[i]
const value = theme[name]
if (value == null) continue
if (!variations) {
parsedTheme[name] = { base: intToHex(colorToInt(value)) }
} else if (isItem) {
/* istanbul ignore else */
if (name === 'base' || name.startsWith('lighten') || name.startsWith('darken')) {
parsedTheme[name] = colorToHex(value)
}
} else if (typeof value === 'object') {
parsedTheme[name] = parse(value, true, variations)
} else {
parsedTheme[name] = genVariations(name, colorToInt(value))
}
}
if (!isItem) {
parsedTheme.anchor = anchor || parsedTheme.base || parsedTheme.primary.base
}
return parsedTheme
}
/**
* Generate the CSS for a base color (.primary)
*/
const genBaseColor = (name: string, value: string): string => {
return `
.v-application .${name} {
background-color: ${value} !important;
border-color: ${value} !important;
}
.v-application .${name}--text {
color: ${value} !important;
caret-color: ${value} !important;
}`
}
/**
* Generate the CSS for a variant color (.primary.darken-2)
*/
const genVariantColor = (name: string, variant: string, value: string): string => {
const [type, n] = variant.split(/(\d)/, 2)
return `
.v-application .${name}.${type}-${n} {
background-color: ${value} !important;
border-color: ${value} !important;
}
.v-application .${name}--text.text--${type}-${n} {
color: ${value} !important;
caret-color: ${value} !important;
}`
}
const genColorVariableName = (name: string, variant = 'base'): string => `--v-${name}-${variant}`
const genColorVariable = (name: string, variant = 'base'): string => `var(${genColorVariableName(name, variant)})`
export function genStyles (theme: VuetifyParsedTheme, cssVar = false): string {
const { anchor, ...variant } = theme
const colors = Object.keys(variant)
if (!colors.length) return ''
let variablesCss = ''
let css = ''
const aColor = cssVar ? genColorVariable('anchor') : anchor
css += `.v-application a { color: ${aColor}; }`
cssVar && (variablesCss += ` ${genColorVariableName('anchor')}: ${anchor};\n`)
for (let i = 0; i < colors.length; ++i) {
const name = colors[i]
const value = theme[name]
css += genBaseColor(name, cssVar ? genColorVariable(name) : value.base)
cssVar && (variablesCss += ` ${genColorVariableName(name)}: ${value.base};\n`)
const variants = Object.keys(value)
for (let i = 0; i < variants.length; ++i) {
const variant = variants[i]
const variantValue = value[variant]
if (variant === 'base') continue
css += genVariantColor(name, variant, cssVar ? genColorVariable(name, variant) : variantValue)
cssVar && (variablesCss += ` ${genColorVariableName(name, variant)}: ${variantValue};\n`)
}
}
if (cssVar) {
variablesCss = `:root {\n${variablesCss}}\n\n`
}
return variablesCss + css
}
export function genVariations (name: string, value: ColorInt): Record<string, string> {
const values: Record<string, string> = {
base: intToHex(value),
}
for (let i = 5; i > 0; --i) {
values[`lighten${i}`] = intToHex(lighten(value, i))
}
for (let i = 1; i <= 4; ++i) {
values[`darken${i}`] = intToHex(darken(value, i))
}
return values
}
export function lighten (value: ColorInt, amount: number): ColorInt {
const lab = LAB.fromXYZ(sRGB.toXYZ(value))
lab[0] = lab[0] + amount * 10
return sRGB.fromXYZ(LAB.toXYZ(lab))
}
export function darken (value: ColorInt, amount: number): ColorInt {
const lab = LAB.fromXYZ(sRGB.toXYZ(value))
lab[0] = lab[0] - amount * 10
return sRGB.fromXYZ(LAB.toXYZ(lab))
}