quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
1,035 lines (914 loc) • 26.6 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(() => [
[
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)
if (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 localModel = textToRgb(v)
if (alpha === true && localModel.a === void 0) {
localModel.a = 100
}
localModel.hex = rgbToHex(localModel)
localModel.rgb = rgbToString(localModel)
return Object.assign(localModel, rgbToHsv(localModel))
}
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 hue = Math.round(val)
const rgb = hsvToRgb({
h: hue,
s: model.value.s,
v: model.value.v,
a: hasAlpha.value === true ? model.value.a : void 0
})
model.value.h = hue
updateModel(rgb, change)
}
function onHueChange(val) {
onHue(val, true)
}
function onNumericChange(value, formatModel, max, evt, change) {
if (evt !== void 0) stop(evt)
if (!/^[0-9]+$/.test(value)) {
if (change === true) proxy.$forceUpdate()
return
}
const val = Math.floor(Number(value))
if (val < 0 || val > max) {
if (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 (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 localModel
if (!inp.endsWith(')')) {
return true
} else if (hasAlpha.value !== true && inp.startsWith('rgb(')) {
localModel = inp
.substring(4, inp.length - 1)
.split(',')
.map(n => parseInt(n, 10))
if (
localModel.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(')) {
localModel = inp.substring(5, inp.length - 1).split(',')
if (
localModel.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(localModel[i], 10)
if (v < 0 || v > 255) {
return true
}
localModel[i] = v
}
const v = parseFloat(localModel[3])
if (v < 0 || v > 1) {
return true
}
localModel[3] = v
} else {
return true
}
if (
localModel[0] < 0 ||
localModel[0] > 255 ||
localModel[1] < 0 ||
localModel[1] > 255 ||
localModel[2] < 0 ||
localModel[2] > 255 ||
(hasAlpha.value === true && (localModel[3] < 0 || localModel[3] > 1))
) {
return true
}
rgb = {
r: localModel[0],
g: localModel[1],
b: localModel[2],
a: hasAlpha.value === true ? localModel[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 = []
if (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 => {
if (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
})
]
if (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')
}
if (props.noHeader !== true) child.unshift(getHeader())
if (props.noFooter !== true) child.push(getFooter())
return h(
'div',
{
class: classes.value,
...attributes.value
},
child
)
}
}
})