quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
852 lines (730 loc) • 25 kB
JavaScript
import { h, ref, computed, watch, nextTick, getCurrentInstance } from 'vue'
import TouchPan from '../../directives/touch-pan/TouchPan.js'
import QSlider from '../slider/QSlider.js'
import QIcon from '../icon/QIcon.js'
import QTabs from '../tabs/QTabs.js'
import QTab from '../tabs/QTab.js'
import QTabPanels from '../tab-panels/QTabPanels.js'
import QTabPanel from '../tab-panels/QTabPanel.js'
import useDark, { useDarkProps } from '../../composables/private.use-dark/use-dark.js'
import useRenderCache from '../../composables/use-render-cache/use-render-cache.js'
import { useFormInject, useFormProps } from '../../composables/use-form/private.use-form.js'
import { createComponent } from '../../utils/private.create/create.js'
import { testPattern } from '../../utils/patterns/patterns.js'
import throttle from '../../utils/throttle/throttle.js'
import { stop } from '../../utils/event/event.js'
import { hexToRgb, rgbToHex, rgbToString, textToRgb, rgbToHsv, hsvToRgb, luminosity } from '../../utils/colors/colors.js'
import { hDir } from '../../utils/private.render/render.js'
const palette = [
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)', 'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)', 'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)', 'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
]
const thumbPath = 'M5 5 h10 v10 h-10 v-10 z'
const alphaTrackImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAH0lEQVQoU2NkYGAwZkAFZ5G5jPRRgOYEVDeB3EBjBQBOZwTVugIGyAAAAABJRU5ErkJggg=='
export default createComponent({
name: 'QColor',
props: {
...useDarkProps,
...useFormProps,
modelValue: String,
defaultValue: String,
defaultView: {
type: String,
default: 'spectrum',
validator: v => [ 'spectrum', 'tune', 'palette' ].includes(v)
},
formatModel: {
type: String,
default: 'auto',
validator: v => [ 'auto', 'hex', 'rgb', 'hexa', 'rgba' ].includes(v)
},
palette: Array,
noHeader: Boolean,
noHeaderTabs: Boolean,
noFooter: Boolean,
square: Boolean,
flat: Boolean,
bordered: Boolean,
disable: Boolean,
readonly: Boolean
},
emits: [ 'update:modelValue', 'change' ],
setup (props, { emit }) {
const { proxy } = getCurrentInstance()
const { $q } = proxy
const isDark = useDark(props, $q)
const { getCache } = useRenderCache()
const spectrumRef = ref(null)
const errorIconRef = ref(null)
const forceHex = computed(() => (
props.formatModel === 'auto'
? null
: props.formatModel.indexOf('hex') !== -1
))
const forceAlpha = computed(() => (
props.formatModel === 'auto'
? null
: props.formatModel.indexOf('a') !== -1
))
const topView = ref(
props.formatModel === 'auto'
? (
(props.modelValue === void 0 || props.modelValue === null || props.modelValue === '' || props.modelValue.startsWith('#'))
? 'hex'
: 'rgb'
)
: (props.formatModel.startsWith('hex') ? 'hex' : 'rgb')
)
const view = ref(props.defaultView)
const model = ref(parseModel(props.modelValue || props.defaultValue))
const editable = computed(() => props.disable !== true && props.readonly !== true)
const isHex = computed(() =>
props.modelValue === void 0
|| props.modelValue === null
|| props.modelValue === ''
|| props.modelValue.startsWith('#')
)
const isOutputHex = computed(() => (
forceHex.value !== null
? forceHex.value
: isHex.value
))
const formAttrs = computed(() => ({
type: 'hidden',
name: props.name,
value: model.value[ isOutputHex.value === true ? 'hex' : 'rgb' ]
}))
const injectFormInput = useFormInject(formAttrs)
const hasAlpha = computed(() => (
forceAlpha.value !== null
? forceAlpha.value
: model.value.a !== void 0
))
const currentBgColor = computed(() => ({
backgroundColor: model.value.rgb || '#000'
}))
const headerClass = computed(() => {
const light = model.value.a !== void 0 && model.value.a < 65
? true
: luminosity(model.value) > 0.4
return 'q-color-picker__header-content'
+ ` q-color-picker__header-content--${ light ? 'light' : 'dark' }`
})
const spectrumStyle = computed(() => ({
background: `hsl(${ model.value.h },100%,50%)`
}))
const spectrumPointerStyle = computed(() => ({
top: `${ 100 - model.value.v }%`,
[ $q.lang.rtl === true ? 'right' : 'left' ]: `${ model.value.s }%`
}))
const computedPalette = computed(() => (
props.palette !== void 0 && props.palette.length !== 0
? props.palette
: palette
))
const classes = computed(() =>
'q-color-picker'
+ (props.bordered === true ? ' q-color-picker--bordered' : '')
+ (props.square === true ? ' q-color-picker--square no-border-radius' : '')
+ (props.flat === true ? ' q-color-picker--flat no-shadow' : '')
+ (props.disable === true ? ' disabled' : '')
+ (isDark.value === true ? ' q-color-picker--dark q-dark' : '')
)
const attributes = computed(() => (
props.disable === true
? { 'aria-disabled': 'true' }
: {}
))
const spectrumDirective = computed(() => {
// if editable.value === true
return [ [
TouchPan,
onSpectrumPan,
void 0,
{ prevent: true, stop: true, mouse: true }
] ]
})
watch(() => props.modelValue, v => {
const localModel = parseModel(v || props.defaultValue)
if (localModel.hex !== model.value.hex) {
model.value = localModel
}
})
watch(() => props.defaultValue, v => {
if (!props.modelValue && v) {
const localModel = parseModel(v)
if (localModel.hex !== model.value.hex) {
model.value = localModel
}
}
})
function updateModel (rgb, change) {
// update internally
model.value.hex = rgbToHex(rgb)
model.value.rgb = rgbToString(rgb)
model.value.r = rgb.r
model.value.g = rgb.g
model.value.b = rgb.b
model.value.a = rgb.a
const value = model.value[ isOutputHex.value === true ? 'hex' : 'rgb' ]
// emit new value
emit('update:modelValue', value)
change === true && emit('change', value)
}
function parseModel (v) {
const alpha = forceAlpha.value !== void 0
? forceAlpha.value
: (
props.formatModel === 'auto'
? null
: props.formatModel.indexOf('a') !== -1
)
if (typeof v !== 'string' || v.length === 0 || testPattern.anyColor(v.replace(/ /g, '')) !== true) {
return {
h: 0,
s: 0,
v: 0,
r: 0,
g: 0,
b: 0,
a: alpha === true ? 100 : void 0,
hex: void 0,
rgb: void 0
}
}
const model = textToRgb(v)
if (alpha === true && model.a === void 0) {
model.a = 100
}
model.hex = rgbToHex(model)
model.rgb = rgbToString(model)
return Object.assign(model, rgbToHsv(model))
}
function changeSpectrum (left, top, change) {
const panel = spectrumRef.value
if (panel === null) return
const
width = panel.clientWidth,
height = panel.clientHeight,
rect = panel.getBoundingClientRect()
let x = Math.min(width, Math.max(0, left - rect.left))
if ($q.lang.rtl === true) {
x = width - x
}
const
y = Math.min(height, Math.max(0, top - rect.top)),
s = Math.round(100 * x / width),
v = Math.round(100 * Math.max(0, Math.min(1, -(y / height) + 1))),
rgb = hsvToRgb({
h: model.value.h,
s,
v,
a: hasAlpha.value === true ? model.value.a : void 0
})
model.value.s = s
model.value.v = v
updateModel(rgb, change)
}
function onHue (val, change) {
const h = Math.round(val)
const rgb = hsvToRgb({
h,
s: model.value.s,
v: model.value.v,
a: hasAlpha.value === true ? model.value.a : void 0
})
model.value.h = h
updateModel(rgb, change)
}
function onHueChange (val) {
onHue(val, true)
}
function onNumericChange (value, formatModel, max, evt, change) {
evt !== void 0 && stop(evt)
if (!/^[0-9]+$/.test(value)) {
change === true && proxy.$forceUpdate()
return
}
const val = Math.floor(Number(value))
if (val < 0 || val > max) {
change === true && proxy.$forceUpdate()
return
}
const rgb = {
r: formatModel === 'r' ? val : model.value.r,
g: formatModel === 'g' ? val : model.value.g,
b: formatModel === 'b' ? val : model.value.b,
a: hasAlpha.value === true
? (formatModel === 'a' ? val : model.value.a)
: void 0
}
if (formatModel !== 'a') {
const hsv = rgbToHsv(rgb)
model.value.h = hsv.h
model.value.s = hsv.s
model.value.v = hsv.v
}
updateModel(rgb, change)
if (evt !== void 0 && change !== true && evt.target.selectionEnd !== void 0) {
const index = evt.target.selectionEnd
nextTick(() => {
evt.target.setSelectionRange(index, index)
})
}
}
function onEditorChange (evt, change) {
let rgb
const inp = evt.target.value
stop(evt)
if (topView.value === 'hex') {
if (
inp.length !== (hasAlpha.value === true ? 9 : 7)
|| !/^#[0-9A-Fa-f]+$/.test(inp)
) {
return true
}
rgb = hexToRgb(inp)
}
else {
let model
if (!inp.endsWith(')')) {
return true
}
else if (hasAlpha.value !== true && inp.startsWith('rgb(')) {
model = inp.substring(4, inp.length - 1).split(',').map(n => parseInt(n, 10))
if (
model.length !== 3
|| !/^rgb\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3}\)$/.test(inp)
) {
return true
}
}
else if (hasAlpha.value === true && inp.startsWith('rgba(')) {
model = inp.substring(5, inp.length - 1).split(',')
if (
model.length !== 4
|| !/^rgba\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},(0|0\.[0-9]+[1-9]|0\.[1-9]+|1)\)$/.test(inp)
) {
return true
}
for (let i = 0; i < 3; i++) {
const v = parseInt(model[ i ], 10)
if (v < 0 || v > 255) {
return true
}
model[ i ] = v
}
const v = parseFloat(model[ 3 ])
if (v < 0 || v > 1) {
return true
}
model[ 3 ] = v
}
else {
return true
}
if (
model[ 0 ] < 0 || model[ 0 ] > 255
|| model[ 1 ] < 0 || model[ 1 ] > 255
|| model[ 2 ] < 0 || model[ 2 ] > 255
|| (hasAlpha.value === true && (model[ 3 ] < 0 || model[ 3 ] > 1))
) {
return true
}
rgb = {
r: model[ 0 ],
g: model[ 1 ],
b: model[ 2 ],
a: hasAlpha.value === true
? model[ 3 ] * 100
: void 0
}
}
const hsv = rgbToHsv(rgb)
model.value.h = hsv.h
model.value.s = hsv.s
model.value.v = hsv.v
updateModel(rgb, change)
if (change !== true) {
const index = evt.target.selectionEnd
nextTick(() => {
evt.target.setSelectionRange(index, index)
})
}
}
function onPalettePick (color) {
const def = parseModel(color)
const rgb = { r: def.r, g: def.g, b: def.b, a: def.a }
if (rgb.a === void 0) {
rgb.a = model.value.a
}
model.value.h = def.h
model.value.s = def.s
model.value.v = def.v
updateModel(rgb, true)
}
function onSpectrumPan (evt) {
if (evt.isFinal) {
changeSpectrum(
evt.position.left,
evt.position.top,
true
)
}
else {
onSpectrumChange(evt)
}
}
const onSpectrumChange = throttle(
evt => { changeSpectrum(evt.position.left, evt.position.top) },
20
)
function onSpectrumClick (evt) {
changeSpectrum(
evt.pageX - window.pageXOffset,
evt.pageY - window.pageYOffset,
true
)
}
function onActivate (evt) {
changeSpectrum(
evt.pageX - window.pageXOffset,
evt.pageY - window.pageYOffset
)
}
function updateErrorIcon (val) {
// we MUST avoid vue triggering a render,
// so manually changing this
if (errorIconRef.value !== null) {
errorIconRef.value.$el.style.opacity = val ? 1 : 0
}
}
function setTopView (val) {
topView.value = val
}
function getHeader () {
const child = []
props.noHeaderTabs !== true && child.push(
h(QTabs, {
class: 'q-color-picker__header-tabs',
modelValue: topView.value,
dense: true,
align: 'justify',
'onUpdate:modelValue': setTopView
}, () => [
h(QTab, {
label: 'HEX' + (hasAlpha.value === true ? 'A' : ''),
name: 'hex',
ripple: false
}),
h(QTab, {
label: 'RGB' + (hasAlpha.value === true ? 'A' : ''),
name: 'rgb',
ripple: false
})
])
)
child.push(
h('div', {
class: 'q-color-picker__header-banner row flex-center no-wrap'
}, [
h('input', {
class: 'fit',
value: model.value[ topView.value ],
...(editable.value !== true
? { readonly: true }
: {}
),
...getCache('topIn', {
onInput: evt => {
updateErrorIcon(onEditorChange(evt) === true)
},
onChange: stop,
onBlur: evt => {
onEditorChange(evt, true) === true && proxy.$forceUpdate()
updateErrorIcon(false)
}
})
}),
h(QIcon, {
ref: errorIconRef,
class: 'q-color-picker__error-icon absolute no-pointer-events',
name: $q.iconSet.type.negative
})
])
)
return h('div', {
class: 'q-color-picker__header relative-position overflow-hidden'
}, [
h('div', { class: 'q-color-picker__header-bg absolute-full' }),
h('div', {
class: headerClass.value,
style: currentBgColor.value
}, child)
])
}
function getContent () {
return h(QTabPanels, {
modelValue: view.value,
animated: true
}, () => [
h(QTabPanel, {
class: 'q-color-picker__spectrum-tab overflow-hidden',
name: 'spectrum'
}, getSpectrumTab),
h(QTabPanel, {
class: 'q-pa-md q-color-picker__tune-tab',
name: 'tune'
}, getTuneTab),
h(QTabPanel, {
class: 'q-color-picker__palette-tab',
name: 'palette'
}, getPaletteTab)
])
}
function setView (val) {
view.value = val
}
function getFooter () {
return h('div', {
class: 'q-color-picker__footer relative-position overflow-hidden'
}, [
h(QTabs, {
class: 'absolute-full',
modelValue: view.value,
dense: true,
align: 'justify',
'onUpdate:modelValue': setView
}, () => [
h(QTab, {
icon: $q.iconSet.colorPicker.spectrum,
name: 'spectrum',
ripple: false
}),
h(QTab, {
icon: $q.iconSet.colorPicker.tune,
name: 'tune',
ripple: false
}),
h(QTab, {
icon: $q.iconSet.colorPicker.palette,
name: 'palette',
ripple: false
})
])
])
}
function getSpectrumTab () {
const data = {
ref: spectrumRef,
class: 'q-color-picker__spectrum non-selectable relative-position cursor-pointer'
+ (editable.value !== true ? ' readonly' : ''),
style: spectrumStyle.value,
...(editable.value === true
? {
onClick: onSpectrumClick,
onMousedown: onActivate
}
: {}
)
}
const child = [
h('div', { style: { paddingBottom: '100%' } }),
h('div', { class: 'q-color-picker__spectrum-white absolute-full' }),
h('div', { class: 'q-color-picker__spectrum-black absolute-full' }),
h('div', {
class: 'absolute',
style: spectrumPointerStyle.value
}, [
model.value.hex !== void 0
? h('div', { class: 'q-color-picker__spectrum-circle' })
: null
])
]
const sliders = [
h(QSlider, {
class: 'q-color-picker__hue non-selectable',
modelValue: model.value.h,
min: 0,
max: 360,
trackSize: '8px',
innerTrackColor: 'transparent',
selectionColor: 'transparent',
readonly: editable.value !== true,
thumbPath,
'onUpdate:modelValue': onHue,
onChange: onHueChange
})
]
hasAlpha.value === true && sliders.push(
h(QSlider, {
class: 'q-color-picker__alpha non-selectable',
modelValue: model.value.a,
min: 0,
max: 100,
trackSize: '8px',
trackColor: 'white',
innerTrackColor: 'transparent',
selectionColor: 'transparent',
trackImg: alphaTrackImg,
readonly: editable.value !== true,
hideSelection: true,
thumbPath,
...getCache('alphaSlide', {
'onUpdate:modelValue': value => onNumericChange(value, 'a', 100),
onChange: value => onNumericChange(value, 'a', 100, void 0, true)
})
})
)
return [
hDir('div', data, child, 'spec', editable.value, () => spectrumDirective.value),
h('div', { class: 'q-color-picker__sliders' }, sliders)
]
}
function getTuneTab () {
return [
h('div', { class: 'row items-center no-wrap' }, [
h('div', 'R'),
h(QSlider, {
modelValue: model.value.r,
min: 0,
max: 255,
color: 'red',
dark: isDark.value,
readonly: editable.value !== true,
...getCache('rSlide', {
'onUpdate:modelValue': value => onNumericChange(value, 'r', 255),
onChange: value => onNumericChange(value, 'r', 255, void 0, true)
})
}),
h('input', {
value: model.value.r,
maxlength: 3,
readonly: editable.value !== true,
onChange: stop,
...getCache('rIn', {
onInput: evt => onNumericChange(evt.target.value, 'r', 255, evt),
onBlur: evt => onNumericChange(evt.target.value, 'r', 255, evt, true)
})
})
]),
h('div', { class: 'row items-center no-wrap' }, [
h('div', 'G'),
h(QSlider, {
modelValue: model.value.g,
min: 0,
max: 255,
color: 'green',
dark: isDark.value,
readonly: editable.value !== true,
...getCache('gSlide', {
'onUpdate:modelValue': value => onNumericChange(value, 'g', 255),
onChange: value => onNumericChange(value, 'g', 255, void 0, true)
})
}),
h('input', {
value: model.value.g,
maxlength: 3,
readonly: editable.value !== true,
onChange: stop,
...getCache('gIn', {
onInput: evt => onNumericChange(evt.target.value, 'g', 255, evt),
onBlur: evt => onNumericChange(evt.target.value, 'g', 255, evt, true)
})
})
]),
h('div', { class: 'row items-center no-wrap' }, [
h('div', 'B'),
h(QSlider, {
modelValue: model.value.b,
min: 0,
max: 255,
color: 'blue',
readonly: editable.value !== true,
dark: isDark.value,
...getCache('bSlide', {
'onUpdate:modelValue': value => onNumericChange(value, 'b', 255),
onChange: value => onNumericChange(value, 'b', 255, void 0, true)
})
}),
h('input', {
value: model.value.b,
maxlength: 3,
readonly: editable.value !== true,
onChange: stop,
...getCache('bIn', {
onInput: evt => onNumericChange(evt.target.value, 'b', 255, evt),
onBlur: evt => onNumericChange(evt.target.value, 'b', 255, evt, true)
})
})
]),
hasAlpha.value === true ? h('div', { class: 'row items-center no-wrap' }, [
h('div', 'A'),
h(QSlider, {
modelValue: model.value.a,
color: 'grey',
readonly: editable.value !== true,
dark: isDark.value,
...getCache('aSlide', {
'onUpdate:modelValue': value => onNumericChange(value, 'a', 100),
onChange: value => onNumericChange(value, 'a', 100, void 0, true)
})
}),
h('input', {
value: model.value.a,
maxlength: 3,
readonly: editable.value !== true,
onChange: stop,
...getCache('aIn', {
onInput: evt => onNumericChange(evt.target.value, 'a', 100, evt),
onBlur: evt => onNumericChange(evt.target.value, 'a', 100, evt, true)
})
})
]) : null
]
}
function getPaletteTab () {
const fn = color => h('div', {
class: 'q-color-picker__cube col-auto',
style: { backgroundColor: color },
...(
editable.value === true
? getCache('palette#' + color, {
onClick: () => { onPalettePick(color) }
})
: {}
)
})
return [
h('div', {
class: 'row items-center q-color-picker__palette-rows'
+ (editable.value === true ? ' q-color-picker__palette-rows--editable' : '')
}, computedPalette.value.map(fn))
]
}
return () => {
const child = [ getContent() ]
if (props.name !== void 0 && props.disable !== true) {
injectFormInput(child, 'push')
}
props.noHeader !== true && child.unshift(
getHeader()
)
props.noFooter !== true && child.push(
getFooter()
)
return h('div', {
class: classes.value,
...attributes.value
}, child)
}
}
})