quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
869 lines (760 loc) • 24.8 kB
JavaScript
import Vue from 'vue'
import { testPattern } from '../../utils/patterns.js'
import throttle from '../../utils/throttle.js'
import cache from '../../utils/cache.js'
import { stop } from '../../utils/event.js'
import { hexToRgb, rgbToHex, rgbToString, textToRgb, rgbToHsv, hsvToRgb, luminosity } from '../../utils/colors.js'
import DarkMixin from '../../mixins/dark.js'
import FormMixin from '../../mixins/form.js'
import ListenersMixin from '../../mixins/listeners.js'
import TouchPan from '../../directives/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'
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)'
]
export default Vue.extend({
name: 'QColor',
mixins: [ ListenersMixin, DarkMixin, FormMixin ],
directives: {
TouchPan
},
props: {
value: 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,
noFooter: Boolean,
square: Boolean,
flat: Boolean,
bordered: Boolean,
disable: Boolean,
readonly: Boolean
},
data () {
return {
topView: this.formatModel === 'auto'
? (
(this.value === void 0 || this.value === null || this.value === '' || this.value.startsWith('#'))
? 'hex'
: 'rgb'
)
: (this.formatModel.startsWith('hex') ? 'hex' : 'rgb'),
view: this.defaultView,
model: this.__parseModel(this.value || this.defaultValue)
}
},
watch: {
value (v) {
const model = this.__parseModel(v || this.defaultValue)
if (model.hex !== this.model.hex) {
this.model = model
}
},
defaultValue (v) {
if (!this.value && v) {
const model = this.__parseModel(v)
if (model.hex !== this.model.hex) {
this.model = model
}
}
}
},
computed: {
editable () {
return this.disable !== true && this.readonly !== true
},
forceHex () {
return this.formatModel === 'auto'
? null
: this.formatModel.indexOf('hex') > -1
},
forceAlpha () {
return this.formatModel === 'auto'
? null
: this.formatModel.indexOf('a') > -1
},
isHex () {
return this.value === void 0 ||
this.value === null ||
this.value === '' ||
this.value.startsWith('#')
},
isOutputHex () {
return this.forceHex !== null
? this.forceHex
: this.isHex
},
formAttrs () {
return {
type: 'hidden',
name: this.name,
value: this.model[ this.isOutputHex === true ? 'hex' : 'rgb' ]
}
},
hasAlpha () {
if (this.forceAlpha !== null) {
return this.forceAlpha
}
return this.model.a !== void 0
},
currentBgColor () {
return {
backgroundColor: this.model.rgb || '#000'
}
},
headerClass () {
const light = this.model.a !== void 0 && this.model.a < 65
? true
: luminosity(this.model) > 0.4
return `q-color-picker__header-content--${light ? 'light' : 'dark'}`
},
spectrumStyle () {
return {
background: `hsl(${this.model.h},100%,50%)`
}
},
spectrumPointerStyle () {
return {
top: `${100 - this.model.v}%`,
[this.$q.lang.rtl === true ? 'right' : 'left']: `${this.model.s}%`
}
},
computedPalette () {
return this.palette !== void 0 && this.palette.length > 0
? this.palette
: palette
},
classes () {
return 'q-color-picker' +
(this.bordered === true ? ' q-color-picker--bordered' : '') +
(this.square === true ? ' q-color-picker--square no-border-radius' : '') +
(this.flat === true ? ' q-color-picker--flat no-shadow' : '') +
(this.disable === true ? ' disabled' : '') +
(this.isDark === true ? ' q-color-picker--dark q-dark' : '')
},
attrs () {
if (this.disable === true) {
return { 'aria-disabled': 'true' }
}
if (this.readonly === true) {
return { 'aria-readonly': 'true' }
}
}
},
created () {
this.__spectrumChange = throttle(this.__spectrumChange, 20)
},
render (h) {
const child = [ this.__getContent(h) ]
if (this.name !== void 0 && this.disable !== true) {
this.__injectFormInput(child, 'push')
}
this.noHeader !== true && child.unshift(
this.__getHeader(h)
)
this.noFooter !== true && child.push(
this.__getFooter(h)
)
return h('div', {
class: this.classes,
attrs: this.attrs,
on: { ...this.qListeners }
}, child)
},
methods: {
__getHeader (h) {
return h('div', {
staticClass: 'q-color-picker__header relative-position overflow-hidden'
}, [
h('div', { staticClass: 'q-color-picker__header-bg absolute-full' }),
h('div', {
staticClass: 'q-color-picker__header-content absolute-full',
class: this.headerClass,
style: this.currentBgColor
}, [
h(QTabs, {
props: {
value: this.topView,
dense: true,
align: 'justify'
},
on: cache(this, 'topVTab', {
input: val => { this.topView = val }
})
}, [
h(QTab, {
props: {
label: 'HEX' + (this.hasAlpha === true ? 'A' : ''),
name: 'hex',
ripple: false
}
}),
h(QTab, {
props: {
label: 'RGB' + (this.hasAlpha === true ? 'A' : ''),
name: 'rgb',
ripple: false
}
})
]),
h('div', {
staticClass: 'q-color-picker__header-banner row flex-center no-wrap'
}, [
h('input', {
staticClass: 'fit',
domProps: { value: this.model[this.topView] },
attrs: this.editable !== true ? {
readonly: true
} : null,
on: cache(this, 'topIn', {
input: evt => {
this.__updateErrorIcon(this.__onEditorChange(evt) === true)
},
change: stop,
blur: evt => {
this.__onEditorChange(evt, true) === true && this.$forceUpdate()
this.__updateErrorIcon(false)
}
})
}),
h(QIcon, {
ref: 'errorIcon',
staticClass: 'q-color-picker__error-icon absolute no-pointer-events',
props: { name: this.$q.iconSet.type.negative }
})
])
])
])
},
__getContent (h) {
return h(QTabPanels, {
props: {
value: this.view,
animated: true
}
}, [
h(QTabPanel, {
staticClass: 'q-color-picker__spectrum-tab overflow-hidden',
props: { name: 'spectrum' }
}, this.__getSpectrumTab(h)),
h(QTabPanel, {
staticClass: 'q-pa-md q-color-picker__tune-tab',
props: { name: 'tune' }
}, this.__getTuneTab(h)),
h(QTabPanel, {
staticClass: 'q-color-picker__palette-tab',
props: { name: 'palette' }
}, this.__getPaletteTab(h))
])
},
__getFooter (h) {
return h('div', {
staticClass: 'q-color-picker__footer relative-position overflow-hidden'
}, [
h(QTabs, {
staticClass: 'absolute-full',
props: {
value: this.view,
dense: true,
align: 'justify'
},
on: cache(this, 'ftIn', {
input: val => { this.view = val }
})
}, [
h(QTab, {
props: {
icon: this.$q.iconSet.colorPicker.spectrum,
name: 'spectrum',
ripple: false
}
}),
h(QTab, {
props: {
icon: this.$q.iconSet.colorPicker.tune,
name: 'tune',
ripple: false
}
}),
h(QTab, {
props: {
icon: this.$q.iconSet.colorPicker.palette,
name: 'palette',
ripple: false
}
})
])
])
},
__getSpectrumTab (h) {
const thumbPath = 'M5 5 h10 v10 h-10 v-10 z'
return [
h('div', {
ref: 'spectrum',
staticClass: 'q-color-picker__spectrum non-selectable relative-position cursor-pointer',
style: this.spectrumStyle,
class: { readonly: this.editable !== true },
on: this.editable === true
? cache(this, 'spectrT', {
click: this.__spectrumClick,
mousedown: this.__activate
})
: null,
directives: this.editable === true
? cache(this, 'spectrDir', [{
name: 'touch-pan',
modifiers: {
prevent: true,
stop: true,
mouse: true
},
value: this.__spectrumPan
}])
: null
}, [
h('div', { style: { paddingBottom: '100%' } }),
h('div', { staticClass: 'q-color-picker__spectrum-white absolute-full' }),
h('div', { staticClass: 'q-color-picker__spectrum-black absolute-full' }),
h('div', {
staticClass: 'absolute',
style: this.spectrumPointerStyle
}, [
this.model.hex !== void 0 ? h('div', { staticClass: 'q-color-picker__spectrum-circle' }) : null
])
]),
h('div', {
staticClass: 'q-color-picker__sliders'
}, [
h('div', { staticClass: 'q-color-picker__hue non-selectable' }, [
h(QSlider, {
props: {
value: this.model.h,
min: 0,
max: 360,
fillHandleAlways: true,
readonly: this.editable !== true,
thumbPath
},
on: cache(this, 'hueSlide', {
input: this.__onHueChange,
change: val => this.__onHueChange(val, true)
})
})
]),
this.hasAlpha === true
? h('div', { staticClass: 'q-color-picker__alpha non-selectable' }, [
h(QSlider, {
props: {
value: this.model.a,
min: 0,
max: 100,
fillHandleAlways: true,
readonly: this.editable !== true,
thumbPath
},
on: cache(this, 'alphaSlide', {
input: value => this.__onNumericChange(value, 'a', 100),
change: value => this.__onNumericChange(value, 'a', 100, void 0, true)
})
})
])
: null
])
]
},
__getTuneTab (h) {
const attrs = {
inputmode: 'numeric',
maxlength: 3,
readonly: this.editable !== true
}
return [
h('div', { staticClass: 'row items-center no-wrap' }, [
h('div', ['R']),
h(QSlider, {
props: {
value: this.model.r,
min: 0,
max: 255,
color: 'red',
dark: this.isDark,
readonly: this.editable !== true
},
on: cache(this, 'rSlide', {
input: value => this.__onNumericChange(value, 'r', 255),
change: value => this.__onNumericChange(value, 'r', 255, void 0, true)
})
}),
h('input', {
domProps: { value: this.model.r },
attrs,
on: cache(this, 'rIn', {
input: evt => this.__onNumericChange(evt.target.value, 'r', 255, evt),
change: stop,
blur: evt => this.__onNumericChange(evt.target.value, 'r', 255, evt, true)
})
})
]),
h('div', { staticClass: 'row items-center no-wrap' }, [
h('div', ['G']),
h(QSlider, {
props: {
value: this.model.g,
min: 0,
max: 255,
color: 'green',
dark: this.isDark,
readonly: this.editable !== true
},
on: cache(this, 'gSlide', {
input: value => this.__onNumericChange(value, 'g', 255),
change: value => this.__onNumericChange(value, 'g', 255, void 0, true)
})
}),
h('input', {
domProps: { value: this.model.g },
attrs,
on: cache(this, 'gIn', {
input: evt => this.__onNumericChange(evt.target.value, 'g', 255, evt),
change: stop,
blur: evt => this.__onNumericChange(evt.target.value, 'g', 255, evt, true)
})
})
]),
h('div', { staticClass: 'row items-center no-wrap' }, [
h('div', ['B']),
h(QSlider, {
props: {
value: this.model.b,
min: 0,
max: 255,
color: 'blue',
readonly: this.editable !== true,
dark: this.isDark
},
on: cache(this, 'bSlide', {
input: value => this.__onNumericChange(value, 'b', 255),
change: value => this.__onNumericChange(value, 'b', 255, void 0, true)
})
}),
h('input', {
domProps: { value: this.model.b },
attrs,
on: cache(this, 'bIn', {
input: evt => this.__onNumericChange(evt.target.value, 'b', 255, evt),
change: stop,
blur: evt => this.__onNumericChange(evt.target.value, 'b', 255, evt, true)
})
})
]),
this.hasAlpha === true ? h('div', { staticClass: 'row items-center no-wrap' }, [
h('div', ['A']),
h(QSlider, {
props: {
value: this.model.a,
color: 'grey',
readonly: this.editable !== true,
dark: this.isDark
},
on: cache(this, 'aSlide', {
input: value => this.__onNumericChange(value, 'a', 100),
change: value => this.__onNumericChange(value, 'a', 100, void 0, true)
})
}),
h('input', {
domProps: { value: this.model.a },
attrs,
on: cache(this, 'aIn', {
input: evt => this.__onNumericChange(evt.target.value, 'a', 100, evt),
change: stop,
blur: evt => this.__onNumericChange(evt.target.value, 'a', 100, evt, true)
})
})
]) : null
]
},
__getPaletteTab (h) {
return [
h('div', {
staticClass: 'row items-center q-color-picker__palette-rows',
class: this.editable === true
? 'q-color-picker__palette-rows--editable'
: ''
}, this.computedPalette.map(color => h('div', {
staticClass: 'q-color-picker__cube col-auto',
style: { backgroundColor: color },
on: this.editable === true ? cache(this, 'palette#' + color, {
click: () => {
this.__onPalettePick(color)
}
}) : null
})))
]
},
__onSpectrumChange (left, top, change) {
const panel = this.$refs.spectrum
if (panel === void 0) { return }
const
width = panel.clientWidth,
height = panel.clientHeight,
rect = panel.getBoundingClientRect()
let x = Math.min(width, Math.max(0, left - rect.left))
if (this.$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: this.model.h,
s,
v,
a: this.hasAlpha === true ? this.model.a : void 0
})
this.model.s = s
this.model.v = v
this.__update(rgb, change)
},
__onHueChange (h, change) {
h = Math.round(h)
const rgb = hsvToRgb({
h,
s: this.model.s,
v: this.model.v,
a: this.hasAlpha === true ? this.model.a : void 0
})
this.model.h = h
this.__update(rgb, change)
},
__onNumericChange (value, formatModel, max, evt, change) {
evt !== void 0 && stop(evt)
if (!/^[0-9]+$/.test(value)) {
change === true && this.$forceUpdate()
return
}
const val = Math.floor(Number(value))
if (val < 0 || val > max) {
change === true && this.$forceUpdate()
return
}
const rgb = {
r: formatModel === 'r' ? val : this.model.r,
g: formatModel === 'g' ? val : this.model.g,
b: formatModel === 'b' ? val : this.model.b,
a: this.hasAlpha === true
? (formatModel === 'a' ? val : this.model.a)
: void 0
}
if (formatModel !== 'a') {
const hsv = rgbToHsv(rgb)
this.model.h = hsv.h
this.model.s = hsv.s
this.model.v = hsv.v
}
this.__update(rgb, change)
if (evt !== void 0 && change !== true && evt.target.selectionEnd !== void 0) {
const index = evt.target.selectionEnd
this.$nextTick(() => {
evt.target.setSelectionRange(index, index)
})
}
},
__onEditorChange (evt, change) {
let rgb
const inp = evt.target.value
stop(evt)
if (this.topView === 'hex') {
if (
inp.length !== (this.hasAlpha === true ? 9 : 7) ||
!/^#[0-9A-Fa-f]+$/.test(inp)
) {
return true
}
rgb = hexToRgb(inp)
}
else {
let model
if (!inp.endsWith(')')) {
return true
}
else if (this.hasAlpha !== 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 (this.hasAlpha === 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 ||
(this.hasAlpha === true && (model[3] < 0 || model[3] > 1))
) {
return true
}
rgb = {
r: model[0],
g: model[1],
b: model[2],
a: this.hasAlpha === true
? model[3] * 100
: void 0
}
}
const hsv = rgbToHsv(rgb)
this.model.h = hsv.h
this.model.s = hsv.s
this.model.v = hsv.v
this.__update(rgb, change)
if (change !== true) {
const index = evt.target.selectionEnd
this.$nextTick(() => {
evt.target.setSelectionRange(index, index)
})
}
},
__onPalettePick (color) {
const def = this.__parseModel(color)
const rgb = { r: def.r, g: def.g, b: def.b, a: def.a }
if (rgb.a === void 0) {
rgb.a = this.model.a
}
this.model.h = def.h
this.model.s = def.s
this.model.v = def.v
this.__update(rgb, true)
},
__update (rgb, change) {
// update internally
this.model.hex = rgbToHex(rgb)
this.model.rgb = rgbToString(rgb)
this.model.r = rgb.r
this.model.g = rgb.g
this.model.b = rgb.b
this.model.a = rgb.a
const value = this.model[this.isOutputHex === true ? 'hex' : 'rgb']
// emit new value
this.$emit('input', value)
change === true && this.$emit('change', value)
},
__updateErrorIcon (val) {
// we MUST avoid vue triggering a render,
// so manually changing this
if (this.$refs.errorIcon !== void 0) {
this.$refs.errorIcon.$el.style.opacity = val ? 1 : 0
}
},
__parseModel (v) {
const forceAlpha = this.forceAlpha !== void 0
? this.forceAlpha
: (
this.formatModel === 'auto'
? null
: this.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: forceAlpha === true ? 100 : void 0,
hex: void 0,
rgb: void 0
}
}
const model = textToRgb(v)
if (forceAlpha === true && model.a === void 0) {
model.a = 100
}
model.hex = rgbToHex(model)
model.rgb = rgbToString(model)
return Object.assign(model, rgbToHsv(model))
},
__spectrumPan (evt) {
if (evt.isFinal) {
this.__onSpectrumChange(
evt.position.left,
evt.position.top,
true
)
}
else {
this.__spectrumChange(evt)
}
},
// throttled in created()
__spectrumChange (evt) {
this.__onSpectrumChange(
evt.position.left,
evt.position.top
)
},
__spectrumClick (evt) {
this.__onSpectrumChange(
evt.pageX - window.pageXOffset,
evt.pageY - window.pageYOffset,
true
)
},
__activate (evt) {
this.__onSpectrumChange(
evt.pageX - window.pageXOffset,
evt.pageY - window.pageYOffset
)
}
}
})