Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ColorPicker #404

Draft
wants to merge 11 commits into
base: dev
Choose a base branch
from
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"eslintIntegration": true,
"singleQuote": true,
"semi": false
}
12 changes: 12 additions & 0 deletions packages/veui-loader/src/index.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,13 @@ import slash from 'slash'
import loaderUtils from 'loader-utils'
import { kebabCase, camelCase, pascalCase, getJSON, normalize } from './utils'
import COMPONENTS from 'veui/components.json'
import getDebug from "debug"

const debug = getDebug('veui-loader:')
const debugFilterKeyword = (process.env.DEBUG_FILTER_KEYWORD || '').toLowerCase()
const debugFilter = function (modulePath) {
return debugFilterKeyword ? modulePath.toLowerCase().indexOf(debugFilterKeyword) >= 0 : true
}

const COMPONENTS_DIRNAME = 'components'
const EXT_TYPES = {
@@ -179,6 +186,7 @@ function getParts (component, options) {
template: fileName
})
let peerPath = slash(path.join(pack, packPath, peerComponent))
if (debugFilter(peerPath)) debug(`Peer path: ${peerPath}`)
pushPart(acc, { path: peerPath })
return acc
},
@@ -278,7 +286,11 @@ async function assurePath (modulePath, resolve) {
if (typeof resolve === 'function') {
try {
resolveCache[modulePath] = !!(await resolve(modulePath))
if (!resolveCache[modulePath]) {
if (debugFilter(modulePath)) debug(`Not found: ${modulePath}`)
}
} catch (e) {
if (debugFilter(modulePath)) debug(`Can not resolve module: ${modulePath}`)
resolveCache[modulePath] = false
}
}
10 changes: 10 additions & 0 deletions packages/veui-theme-one/components/ColorPalette.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

import config from 'veui/managers/config'

config.defaults({
ui: {
size: {
values: ['small']
}
}
}, 'colorpalette')
26 changes: 26 additions & 0 deletions packages/veui-theme-one/components/ColorPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

import config from 'veui/managers/config'

config.defaults({
ui: {
size: {
values: ['small'],
data: {
default: {
shadeFieldSize: [294, 294]
},
small: {
shadeFieldSize: [248, 180]
}
}
},
swatch: {
boolean: true,
default: false
},
tip: {
boolean: true,
default: false
}
}
}, 'colorpicker')
14 changes: 14 additions & 0 deletions packages/veui-theme-one/components/ColorSwatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import config from 'veui/managers/config'

config.defaults({
ui: {
size: {
values: ['small']
},
tip: {
boolean: true,
default: false
}
}
}, 'colorswatch')
68 changes: 68 additions & 0 deletions packages/veui-theme-one/components/color-palette.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@import "../lib.less";

.veui-color-palette {
padding: 10px 15px;
border-top: 1px solid @veui-gray-color-8;

&-colors {
.clearfix();
}

&-color {
float: left;
margin: 5px;
width: @veui-height-tiny;
height: @veui-height-tiny;
border: 1px solid fade(@veui-gray-color-3, 20%);
border-radius: 4px;

div {
width: 100%;
height: 100%;
}

& > div {
.veui-transparency-grid-background();
}

// add button
line-height: @veui-height-tiny;
text-align: center;
background: @veui-gray-color-6;

path {
fill: @veui-gray-color-5;
}

&-outside {
opacity: .7;
position: relative;
z-index: 1;

&::before {
content: "移除";
position: absolute;
font-size: 12px;
width: 40px;
top: -20px;
left: -10px;
}
}

&-putback {
transition: transform 300ms;
}
}

&[ui~="small"] {
padding: 6px;

.veui-color-palette-color {
margin: 4px;
width: 17px;
height: 17px;
line-height: 17px;
border-radius: 2px;
}
}
}
101 changes: 101 additions & 0 deletions packages/veui-theme-one/components/color-picker.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@import "../lib.less";

.veui-color-shade-field {
position: relative;

&-shade {
width: 100%;
height: 100%;
}

&-aperture {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 12px;
border: 2px solid @veui-gray-color-5;
border-radius: 100%;
box-shadow: 0 2px 4px 0 fade(#000, 50%);
}
}

.veui-color-slider {
margin: 5px 0;
padding: 3px 0;

.veui-slider {
height: 18px;

.veui-slider-thumb {
top: -2px;
}

.veui-slider-custom-thumb {
width: 8px;
height: 22px;
border: 1px solid rgba(51, 51, 51, .8);
background: rgba(255, 255, 255, .9);
box-shadow: 1px 1px 4px rgba(0, 0, 0, .8);
border-radius: 2px;
}

.veui-slider-custom-track {
width: 100%;
height: 18px;
}
}
}

.veui-color-hue-slider {
.veui-slider-custom-track {
background: url("data:image/svg+xml;utf8;<svg xmlns='http://www.w3.org/2000/svg'><defs><linearGradient id='a' x1='0%' y1='0%' y2='0%'><stop offset='0%' stop-color='red'/><stop offset='16.7%' stop-color='%23ff0'/><stop offset='33.3%' stop-color='%230f0'/><stop offset='50%' stop-color='%230ff'/><stop offset='66.7%' stop-color='%2300f'/><stop offset='83.3%' stop-color='%23f0f'/><stop offset='100%' stop-color='red'/></linearGradient></defs><rect width='100%' height='100%' fill='url(%23a)'/></svg>");
background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}
}

.veui-color-alpha-slider {
.veui-slider-track {
.veui-transparency-grid-background();
}
}

.veui-color-picker {
width: 336px;
color: #333;
border: 1px solid @veui-gray-color-3;
box-shadow: 0 1px 5px 0 fade(#000, 20%);
border-radius: 2px;

&-main {
padding: 20px;

&-panel {
&-sliders {
margin-top: 15px;
}
}

.veui-color-swatch {
margin-top: 15px;
}
}

&[ui~="small"] {
width: 250px + 20px;

.veui-color-picker-main {
padding: 10px;

&-panel {
&-sliders {
margin-top: 10px;
}
}

.veui-color-swatch {
margin-top: 10px;
}
}
}
}
273 changes: 273 additions & 0 deletions packages/veui-theme-one/components/color-swatch.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
@import "../lib.less";

.veui-color-swatch {
@height: 40px;

.clearfix();

&-box,
.veui-color-value-alpha-group {
float: left;
height: @height;
line-height: @height;
}

&-box {
width: @height;
border: 1px solid fade(@veui-gray-color-3, 20%);
border-radius: 2px;

div {
height: 100%;
}

&-bg {
.veui-transparency-grid-background();
}
}

&[ui~="small"] {
@height: 30px;
@space-width: 10px;
@input-width: 45px;

.veui-color-swatch-box,
.veui-color-value-alpha-group {
height: @height;
line-height: @height;
}

.veui-color-swatch-box {
width: @height;
}

.veui-color-value-hex {
.veui-color-value {
width: @input-width * 3 + 5 * 2;
}
}

.veui-color-value-hsl,
.veui-color-value-rgb,
.veui-color-value-alpha {
.veui-color-value {
width: @input-width;
}
}

.veui-color-value-alpha-group {
&-color,
&-alpha {
margin-left: @space-width;
}

&-alpha {
margin-left: @space-width + 8px;
}

&-separator {
&-wrap {
width: @space-width + 8px;
}

&-dots {
div {
margin: 4px;
width: 4px;
height: 4px;
}
}
}

&-tip {
&-hsl,
&-rgb,
&-hex {
float: left;
margin-left: @space-width;
}

&-hsl,
&-rgb {
div {
width: @input-width;
}
}

&-hex {
width: @input-width * 3 + 5 * 2;
}

&-alpha {
float: right;
width: @input-width;
}
}
}
}
}

.veui-color-value {
.veui-input {
width: 100%;
}
}

.veui-color-value-hex {
.veui-input-input {
text-transform: uppercase;
letter-spacing: 1px;
}

.veui-color-value {
width: 160px;
}
}

.veui-color-value-hsl,
.veui-color-value-rgb,
.veui-color-value-alpha {
.clearfix();

.veui-input-input {
padding: 0;
text-align: center;
}

.veui-color-value {
float: left;
width: 50px;
margin-left: 5px;

&:first-child {
margin-left: 0;
}
}
}

.veui-color-value-alpha-group {
@space-width: 18px;

&-values {
.clearfix();

&,
div {
height: 100%;
}
}

&-color,
&-alpha {
margin-left: @space-width;
float: left;
}

&-alpha {
margin-left: @space-width + 8px;
}

&-separator {
float: left;
height: 100%;
position: relative;

&-wrap {
position: absolute;
top: 0;
left: 0;
width: @space-width + 8px;

div {
height: auto;
}
}

&-dots {
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);

div {
margin: 4px;
width: 4px;
height: 4px;
border-radius: 100%;
background-color: @veui-gray-color-3;
}
}
}

&-format-hex {
.veui-color-value-alpha-group-separator-dot {
&:nth-child(1) {
background-color: @veui-gray-color-2;
}
}
}

&-format-rgb {
.veui-color-value-alpha-group-separator-dot {
&:nth-child(2) {
background-color: @veui-gray-color-2;
}
}
}

&-format-hsl {
.veui-color-value-alpha-group-separator-dot {
&:nth-child(3) {
background-color: @veui-gray-color-2;
}
}
}

&-show-tip {
position: relative;
padding-bottom: 16px;
box-sizing: content-box;
}

&-tip {
.clearfix();
color: @veui-gray-color-2;
font-size: @veui-font-size-small;
text-align: center;
line-height: 1;

position: absolute;
bottom: 0;
left: 0;
right: 0;

&-hsl,
&-rgb,
&-hex {
float: left;
margin-left: 15px;
}

&-hsl,
&-rgb {
div {
float: left;
width: 50px;
margin-left: 5px;

&:first-child {
margin-left: 0;
}
}
}

&-hex {
width: 160px;
}

&-alpha {
float: right;
width: 50px;
}
}
}
4 changes: 4 additions & 0 deletions packages/veui-theme-one/mixins.less
Original file line number Diff line number Diff line change
@@ -67,3 +67,7 @@
outline-offset: @offset;
box-shadow: none;
}

.veui-transparency-grid-background() {
background: #fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='5' height='5' x='0' y='0' fill='%23ccc'/><rect width='5' height='5' x='5' y='5' fill='%23ccc'/></svg>");
}
29 changes: 22 additions & 7 deletions packages/veui-theme-one/package-lock.json
1 change: 1 addition & 0 deletions packages/veui-theme-one/package.json
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.2.1",
"babel-preset-stage-2": "^6.22.0",
"debug": "^4.1.1",
"esm": "^3.0.84",
"mkdirp": "^0.5.1",
"rimraf": "^2.6.2",
16 changes: 16 additions & 0 deletions packages/veui/components.json
Original file line number Diff line number Diff line change
@@ -43,6 +43,22 @@
"name": "CheckboxGroup",
"path": "CheckboxGroup.vue"
},
{
"name": "ColorPalette",
"path": "ColorPicker/ColorPalette.vue"
},
{
"name": "ColorPicker",
"path": "ColorPicker/ColorPicker.vue"
},
{
"name": "ColorSwatch",
"path": "ColorPicker/ColorSwatch.vue"
},
{
"name": "ColorPicker",
"path": "ColorPicker.js"
},
{
"name": "Column",
"path": "Column.js"
2 changes: 1 addition & 1 deletion packages/veui/config/index.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ module.exports = {
dev: {
env: require('./dev.env'),
port: 8080,
autoOpenBrowser: true,
autoOpenBrowser: false,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
260 changes: 260 additions & 0 deletions packages/veui/demo/cases/ColorPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
<template>
<article>
<h1>
<code>&lt;veui-color-picker&gt;</code>
</h1>

<div class="tip">
当前颜色
</div>
<div
class="color-text"
:style="{ color }"
>
{{ color }}
</div>

<div class="color-control">
<label>
Variant
<select v-model="variant">
<option
v-for="item in variants"
:key="item"
:value="item"
>
{{ item }}
</option>
</select>
</label>

<label>
<input
v-model="alpha"
type="checkbox"
>
alpha channel
</label>

<label>
<input
v-model="switchable"
type="checkbox"
>
switchable
</label>
</div>

<h2>色样</h2>
<section class="color-swatches">
<veui-color-swatch
v-model="color"
ui="small"
v-bind="{variant, alpha, switchable}"
/>
<veui-color-swatch
v-model="color"
ui="small tip"
v-bind="{variant, alpha, switchable}"
/>
<veui-color-swatch
v-model="color"
v-bind="{variant, alpha, switchable}"
/>
<veui-color-swatch
v-model="color"
ui="tip"
v-bind="{variant, alpha, switchable}"
/>
<veui-color-swatch
:color="color"
ui="normal tip"
:readonly="true"
v-bind="{variant, alpha, switchable}"
/>
</section>

<h2>取色器</h2>
<p>
<label>
<input
v-model="showPalette"
type="checkbox"
>
附加色板?
</label>
</p>

<section class="color-pickers">
<section class="color-picker">
<p>
<code>ui="small"</code>
</p>
<veui-color-picker
v-model="color"
ui="small"
v-bind="{variant, alpha, switchable}"
>
<!-- 色板作为 slot 传入 -->
<veui-color-palette
v-if="showPalette"
ui="small"
:colors="colors"
@select="handlePaletteColorSelect"
@remove="handlePaletteColorRemove"
@add="handlePaletteColorAdd"
/>
</veui-color-picker>
</section>

<section class="color-picker">
<p>
<code>ui="swatch tip"</code>
</p>
<veui-color-picker
v-model="color"
ui="swatch tip"
v-bind="{variant, alpha, switchable}"
>
<!-- 色板作为 slot 传入 -->
<veui-color-palette
v-if="showPalette"
:colors="colors"
@select="handlePaletteColorSelect"
@remove="handlePaletteColorRemove"
@add="handlePaletteColorAdd"
>
<div style="margin: 10px 5px 5px 5px;">
<veui-button
ui="aux small"
style="width: 100%"
>
高级选项
</veui-button>
</div>
</veui-color-palette>
</veui-color-picker>
</section>
</section>
</article>
</template>

<script>
import bus from '../bus'
import tinycolor from 'tinycolor2'
import { ColorSwatch, ColorPicker, ColorPalette, Button } from 'veui'
const variants = ['hex', 'rgb', 'hsl']
export default {
name: 'color-picker-demo',
components: {
'veui-button': Button,
'veui-color-swatch': ColorSwatch,
'veui-color-picker': ColorPicker,
'veui-color-palette': ColorPalette
},
data () {
return {
variant: variants[0],
variants,
alpha: true,
switchable: true,
showPalette: true,
color: 'hsla(123, 54%, 43%, 0.9)',
colors: [
'#D0021B',
'#F5A623',
'#F8E71C',
'#8B572A',
'#7ED322',
'#417505',
'#BD10E0',
'#9014FE',
'#4A90E2',
'#50E3C2',
'#B8E986'
]
}
},
computed: {
kolor () {
let { r, g, b, a } = tinycolor(this.color).toRgb()
return tinycolor({
r: 0xff - r,
g: 0xff - g,
b: 0xff - b,
a: a
}).toRgbString()
}
},
mounted () {
this.$children.forEach(child => {
child.$on('click', () => {
bus.$emit('log', child.$el.getAttribute('ui'))
})
})
},
methods: {
handlePaletteColorSelect (i) {
this.color = this.colors[i]
},
handlePaletteColorRemove (i) {
this.colors.splice(i, 1)
},
handlePaletteColorAdd () {
if (tinycolor.equals(this.color, this.colors[this.colors.length - 1])) {
return
}
this.colors.push(this.color)
}
}
}
</script>

<style lang="less" scoped>
section {
margin-bottom: 3em;
}
.color-text {
font-family: monospace;
font-size: 1.4em;
}
.tip {
margin: 12px 0 2px 0;
color: #333;
small {
&:before {
content: '(';
}
&:after {
content: ')';
}
}
}
.color-control {
position: fixed;
right: 30px;
top: 70px;
margin: 1.2em 0;
label {
margin-right: 2em;
}
}
.veui-color-swatch {
margin: 1.2em 0;
}
.color-pickers {
display: flex;
flex-wrap: wrap;
}
.color-picker {
margin: 0 3em 3em 0;
}
</style>
10 changes: 9 additions & 1 deletion packages/veui/demo/cases/index.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import Breadcrumb from './Breadcrumb'
import Input from './Input'
import Form from './Form'
import Calendar from './Calendar'
import ColorPicker from './ColorPicker'
import DatePicker from './DatePicker'
import Select from './Select'
import Dropdown from './Dropdown'
@@ -83,6 +84,11 @@ export default [
name: 'CheckboxGroup',
component: CheckboxGroup
},
{
path: '/color-picker',
name: 'ColorPicker',
component: ColorPicker
},
{
path: '/date-picker',
name: 'DatePicker',
@@ -247,4 +253,6 @@ export default [
name: 'Uploader',
component: Uploader
}
]
].sort(function (a, b) {
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
})
5 changes: 5 additions & 0 deletions packages/veui/package-lock.json
1 change: 1 addition & 0 deletions packages/veui/package.json
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
"lodash": "^4.17.4",
"resize-detector": "^0.1.6",
"tether": "^1.4.5",
"tinycolor2": "^1.4.1",
"vue-awesome": "^3.3.1"
},
"peerDependencies": {
3 changes: 3 additions & 0 deletions packages/veui/src/components/ColorPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ColorPicker from './ColorPicker/ColorPicker'
export default ColorPicker

166 changes: 166 additions & 0 deletions packages/veui/src/components/ColorPicker/ColorPalette.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<template>
<div
class="veui-color-palette"
:ui="realUi"
>
<div
ref="colors"
class="veui-color-palette-colors"
>
<div
v-for="(color, i) in colors"
:key="i"
v-drag
class="veui-color-palette-color"
:class="{
'veui-color-palette-color-outside': i === dragItem.index && dragItem.outside
}"
:style="{
transform: i === dragItem.index
? `translate(${dragItem.distanceX}px, ${dragItem.distanceY}px)`
: ''
}"
:data-index="i"
@click="handleColorClick(i)"
>
<div>
<div
:style="{
'background-color': color
}"
/>
</div>
</div>
<div
class="veui-color-palette-color"
@click="$emit('add')"
>
<veui-icon name="plus"/>
</div>
</div>
<div class="veui-color-palette-extra">
<slot/>
</div>
</div>
</template>

<script>
import Icon from '../Icon'
import { drag } from '../../directives'
import ui from '../../mixins/ui'
const putbackClass = 'veui-color-palette-color-putback'
export default {
name: 'color-palette',
components: {
'veui-icon': Icon
},
directives: {
drag
},
mixins: [ui],
props: {
colors: {
type: Array,
default () {
return []
}
},
ui: String
},
data () {
return {
fieldSize: {
top: 0,
left: 0,
width: 0,
height: 0
},
dragItem: {
index: -1,
top: 0,
left: 0,
distanceX: 0,
distanceY: 0
}
}
},
mounted () {
this.$on('dragstart', ({ event: { currentTarget: target } }) => {
this.mouseupMark = 0
target.classList.remove(putbackClass)
let { top: targetTop, left: targetLeft } = target.getBoundingClientRect()
this.dragItem.top = targetTop
this.dragItem.left = targetLeft
this.dragItem.distanceX = 0
this.dragItem.distanceY = 0
let {
top,
left,
width,
height
} = this.$refs.colors.getBoundingClientRect()
this.fieldSize.top = top
this.fieldSize.left = left
this.fieldSize.width = width
this.fieldSize.height = height
this.dragItem.outside = false
this.dragItem.index = parseInt(target.dataset.index, 10)
this.dragItemNode = target
})
this.$on('dragend', ({ event }) => {
let removeIndex = this.dragItem.index
this.dragItem.index = -1
if (this.dragItem.outside) {
this.$emit('remove', removeIndex)
} else if (this.dragItem.distanceX || this.dragItem.distanceY) {
this.dragItemNode.classList.add(putbackClass)
this.dragItemNode = null
}
})
this.$on('drag', ({ distanceX, distanceY }) => {
this.mouseupMark = 1
this.dragItem.distanceX = distanceX
this.dragItem.distanceY = distanceY
this.dragItem.outside = isDragItemOutsideOfField(
this.dragItem.left + distanceX,
this.dragItem.top + distanceY,
this.fieldSize.top,
this.fieldSize.left,
this.fieldSize.width,
this.fieldSize.height
)
})
},
methods: {
handleColorClick (i) {
// 如果没有拖动(drag),就是点击,否则不处理,防止拖动误判为点击
if (!this.removable && this.mouseupMark) {
return
}
this.$emit(this.removable ? 'remove' : 'select', i)
}
}
}
function isDragItemOutsideOfField (
itemX,
itemY,
fieldTop,
fieldLeft,
fieldWidth,
fieldHeight
) {
let aroundWidth = 30
return (
itemX < fieldLeft - aroundWidth ||
itemX > fieldLeft + fieldWidth + aroundWidth ||
itemY < fieldTop - aroundWidth ||
itemY > fieldTop + fieldHeight + aroundWidth
)
}
</script>
57 changes: 57 additions & 0 deletions packages/veui/src/components/ColorPicker/ColorPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div
class="veui-color-picker"
:ui="realUi"
>
<div class="veui-color-picker-main">
<div class="veui-color-picker-main-panel">
<veui-color-shade-field
:width="shadeFieldSize[0]"
:height="shadeFieldSize[1]"
:ui="realUi"
:color="color"
:hsv="hsv"
/>
<div class="veui-color-picker-main-panel-sliders">
<veui-color-hue-slider :hsl="hsl"/>
<veui-color-alpha-slider
v-if="alpha"
:hsl="hsl"
/>
</div>
</div>
<veui-color-swatch
v-if="uiProps.swatch"
v-bind="{color, ui: realUi, switchable, alpha, variant}"
/>
</div>
<div class="veui-color-picker-extra">
<slot/>
</div>
</div>
</template>

<script>
import ColorSwatch from './ColorSwatch'
import ui from '../../mixins/ui'
import ColorHomer from './mixins/_ColorHomer'
import HueSlider from './_ColorHueSlider'
import AlphaSlider from './_ColorAlphaSlider'
import ShadeField from './_ColorShadeField'
export default {
name: 'color-picker',
components: {
'veui-color-swatch': ColorSwatch,
'veui-color-hue-slider': HueSlider,
'veui-color-alpha-slider': AlphaSlider,
'veui-color-shade-field': ShadeField
},
mixins: [ui, ColorHomer],
computed: {
shadeFieldSize () {
return this.uiData.shadeFieldSize
}
}
}
</script>
44 changes: 44 additions & 0 deletions packages/veui/src/components/ColorPicker/ColorSwatch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div
class="veui-color-swatch"
:ui="realUi"
>
<div class="veui-color-swatch-box">
<div class="veui-color-swatch-box-bg">
<div :style="{'background-color': color}"/>
</div>
</div>
<veui-color-value-alpha-group
v-bind="{
ui,
readonly,
hsl,
rgb,
showTip: !!uiProps.tip,
switchable,
alphaChannel: alpha,
variant
}"
/>
</div>
</template>

<script>
import ValueAlphaGroup from './_ColorValueAlphaGroup'
import ui from '../../mixins/ui'
import ColorHomer from './mixins/_ColorHomer'
export default {
name: 'color-swatch',
components: {
'veui-color-value-alpha-group': ValueAlphaGroup
},
mixins: [ui, ColorHomer],
props: {
readonly: {
type: Boolean,
default: false
}
}
}
</script>
53 changes: 53 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorAlphaSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<div class="veui-color-slider veui-color-alpha-slider">
<veui-slider
:min="0"
:max="1"
:step="0.01"
:value="hsl.a"
@input="handleValueUpdate"
>
<div
slot="track"
class="veui-slider-custom-track"
:style="{ background: gradient }"
/>
<div
slot="thumb"
class="veui-slider-custom-thumb"
/>
<template slot="tip">
&#8203;
</template>
</veui-slider>
</div>
</template>

<script>
import ColorSlider from './mixins/_ColorSlider'
export default {
name: 'color-alpha-slider',
mixins: [
ColorSlider
],
computed: {
gradient () {
let { h, s, l } = this.hsl
let from = `hsla(${h}, ${s * 100}%,${l * 100}%, 0)`
let to = `hsla(${h}, ${s * 100}%,${l * 100}%, 1)`
return [
`url("data:image/svg+xml;utf8;<svg xmlns='http://www.w3.org/2000/svg'><defs><linearGradient id='a' x1='0%' y1='0%' y2='0%'><stop offset='0%' stop-color='${from}'/><stop offset='100%' stop-color='${to}'/></linearGradient></defs><rect width='100%' height='100%' fill='url(%23a)'/></svg>")`,
`linear-gradient(to right, ${from}, ${to})`
]
}
},
methods: {
handleValueUpdate (val) {
this.updateColor({
a: val
})
}
}
}
</script>
59 changes: 59 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorHueSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<div class="veui-color-slider veui-color-hue-slider">
<veui-slider
:min="0"
:max="360"
:step="1"
:value="localHue"
@input="handleValueUpdate"
>
<div
slot="track"
class="veui-slider-custom-track"
/>
<div
slot="thumb"
class="veui-slider-custom-thumb"
/>
<template slot="tip">
&#8203;
</template>
</veui-slider>
</div>
</template>

<script>
import ColorSlider from './mixins/_ColorSlider'
export default {
name: 'color-hue-slider',
mixins: [
ColorSlider
],
data () {
return {
localHue: 0
}
},
watch: {
hsl: {
handler ({h}) {
// Hue 到了 360 时取余归零,为了避免滑块跳变,这里处理一下
if (h || this.localHue % 360) {
this.localHue = h
}
},
deep: true,
immediate: true
}
},
methods: {
handleValueUpdate (val) {
this.localHue = val
this.updateColor({
h: val % 360
})
}
}
}
</script>
179 changes: 179 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorShadeField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<template>
<div
class="veui-color-shade-field"
:style="{
width: width + 'px',
height: height + 'px'
}"
>
<div
ref="field"
class="veui-color-shade-field-shade"
@click="handleShadeFieldClick"
>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
v-bind="{width, height}"
>
<defs>
<linearGradient
:id="'saturation-' + svgSuffix"
x1="0"
y1="0"
x2="1"
y2="0"
>
<stop
offset="0%"
stop-color="#fff"
/>
<stop
offset="100%"
:stop-color="hueColor"
/>
</linearGradient>
<linearGradient
:id="'brightness-' + svgSuffix"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stop-color="#000"
stop-opacity="0"
/>
<stop
offset="100%"
stop-color="#000"
stop-opacity="1"
/>
</linearGradient>
</defs>
<rect
x="0"
y="0"
v-bind="{width, height}"
:fill="`url(#saturation-${svgSuffix})`"
/>
<rect
x="0"
y="0"
v-bind="{width, height}"
:fill="`url(#brightness-${svgSuffix})`"
/>
</svg>
</div>
<div
v-drag
class="veui-color-shade-field-aperture"
:style="{
'background-color': color,
transform: `translate(${dragCurrentX - 6}px, ${dragCurrentY - 6}px)`
}"
@click.stop
/>
</div>
</template>

<script>
// import tinycolor from 'tinycolor2'
import {drag} from '../../directives'
import {clamp} from 'lodash'
import ColorUpdater from './mixins/_ColorUpdater'
import {getTypedAncestorTracker} from '../../utils/helper'
export default {
name: 'color-shade-field',
directives: {
drag
},
mixins: [
ColorUpdater,
getTypedAncestorTracker('color-homer')
],
props: {
width: Number,
height: Number,
color: String,
hsv: Object
},
data () {
return {
isDragging: false,
dragInitX: 0,
dragInitY: 0,
dragCurrentX: 0,
dragCurrentY: 0
}
},
computed: {
svgSuffix () {
// Chrome Document内多个 <svg> 内的 <defs> 定义的 id 是共享的,所以加个后缀防止冲突
return Math.round(Math.random() * 0xFFFFFF).toString(36)
},
aperturePosition () {
let {s, v} = this.hsv
return {
x: s * this.width,
y: (1 - v) * this.height
}
},
hueColor () {
return `hsl(${this.hsv.h}, 100%, 50%)`
}
},
watch: {
aperturePosition: {
handler ({x, y}) {
if (!this.isDragging) {
this.dragCurrentX = x
this.dragCurrentY = y
}
},
immediate: true
},
isDragging (val) {
// 如果拖到底下黑色那块儿,颜色出去转一圈回来 hue 变 0 了,呵呵,锁一下
this.colorHomer.lockHue(val ? this.hsv.h : null)
}
},
mounted () {
this.$on('dragstart', () => {
this.isDragging = true
// 一开始没拖的时候还是要从颜色反推位置
this.dragInitX = this.dragCurrentX === undefined ? this.aperturePosition.x : this.dragCurrentX
this.dragInitY = this.dragCurrentY === undefined ? this.aperturePosition.y : this.dragCurrentY
})
this.$on('dragend', () => {
this.isDragging = false
})
this.$on('drag', ({distanceX, distanceY}) => {
let x = this.dragInitX + distanceX
let y = this.dragInitY + distanceY
// 底下黑色那一块儿颜色都糊在一起,从颜色算出来的坐标就很飘,还是用自己的位置比较稳
this.dragCurrentX = clamp(x, 0, this.width)
this.dragCurrentY = clamp(y, 0, this.height)
// 得合在一起传出去(satlig=saturation+brightness)。因为要到 ColorPicker format成字符串再传回来
// 如果分开的话,后一个到达 ColorPicker 的时候前一个还没生效,所以使用原来的值,导致前一个无法改变
this.updateSatbri(x / this.width, 1 - y / this.height)
})
},
methods: {
handleShadeFieldClick ({clientX, clientY, offsetX, offsetY}) {
this.updateSatbri(offsetX / this.width, 1 - offsetY / this.height)
this.$emit('dragend')
},
updateSatbri (saturation, brightness) {
let s = clamp(saturation, 0, 1)
let v = clamp(brightness, 0, 1)
this.updateColor({s, v})
}
}
}
</script>
33 changes: 33 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueAlpha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<div class="veui-color-value-alpha">
<div class="veui-color-value">
<veui-color-value-input
:value="alpha"
:readonly="readonly"
:format="formatPercentage"
:parse="parsePercentage"
nudge="percentage"
@input="handleValueInput"
/>
</div>
</div>
</template>

<script>
import ColorValueInput from './mixins/_ColorValueInput'
export default {
name: 'color-value-alpha',
mixins: [ColorValueInput],
computed: {
alpha () {
return this.hsl.a === undefined ? 1 : this.hsl.a
}
},
methods: {
handleValueInput (val) {
this.updateColor({a: val})
}
}
}
</script>
125 changes: 125 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueAlphaGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<div
class="veui-color-value-alpha-group"
:class="{
[`veui-color-value-alpha-group-format-${realVariant}`]: true,
['veui-color-value-alpha-group-show-tip']: showTip
}"
>
<div class="veui-color-value-alpha-group-values">
<div class="veui-color-value-alpha-group-color">
<component
:is="'veui-color-value-' + realVariant"
v-bind="{hsl, rgb, readonly}"
/>
</div>
<div
v-if="switchable"
class="veui-color-value-alpha-group-separator"
@click="toggleColorFormatVariant"
>
<div class="veui-color-value-alpha-group-separator-wrap">
<div class="veui-color-value-alpha-group-separator-dots">
<div class="veui-color-value-alpha-group-separator-dot"/>
<div class="veui-color-value-alpha-group-separator-dot"/>
<div class="veui-color-value-alpha-group-separator-dot"/>
</div>
</div>
</div>
<div
v-if="alphaChannel"
class="veui-color-value-alpha-group-alpha"
>
<veui-color-value-alpha
v-bind="{hsl, readonly}"
/>
</div>
</div>
<div
v-if="showTip"
class="veui-color-value-alpha-group-tip"
>
<div
v-if="realVariant === 'rgb'"
class="veui-color-value-alpha-group-tip-rgb"
>
<div>R</div>
<div>G</div>
<div>B</div>
</div>
<div
v-else-if="realVariant === 'hsl'"
class="veui-color-value-alpha-group-tip-hsl"
>
<div>H</div>
<div>S</div>
<div>L</div>
</div>
<div
v-else-if="realVariant === 'hex'"
class="veui-color-value-alpha-group-tip-hex"
>
<div>HEX</div>
</div>
<div
v-if="alphaChannel"
class="veui-color-value-alpha-group-tip-alpha"
>
<div>A</div>
</div>
</div>
</div>
</template>

<script>
import ValueHsl from './_ColorValueHsl'
import ValueRgb from './_ColorValueRgb'
import ValueHex from './_ColorValueHex'
import ValueAlpha from './_ColorValueAlpha'
const variants = ['hex', 'rgb', 'hsl']
export default {
name: 'color-value-alpha-group',
components: {
'veui-color-value-hsl': ValueHsl,
'veui-color-value-rgb': ValueRgb,
'veui-color-value-hex': ValueHex,
'veui-color-value-alpha': ValueAlpha
},
props: {
hsl: Object,
rgb: Object,
readonly: Boolean,
switchable: Boolean,
showTip: Boolean,
alphaChannel: Boolean,
variant: String
},
data () {
return {
realVariant: 'rgb'
}
},
watch: {
realVariant (val) {
this.$emit('update:variant', val)
},
variant: {
handler (val, oldVal) {
if (this.realVariant !== val) {
this.realVariant = val
}
},
immediate: true
}
},
methods: {
toggleColorFormatVariant () {
let i = variants.indexOf(this.realVariant)
i = (i + 1) % variants.length
this.realVariant = variants[i]
}
}
}
</script>
32 changes: 32 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueHex.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<div class="veui-color-value-hex">
<div class="veui-color-value">
<veui-color-value-input
:value="hexValue"
:readonly="readonly"
:parse="parseHexValue"
@input="handleValueInput"
/>
</div>
</div>
</template>

<script>
import tinycolor from 'tinycolor2'
import ColorValueInput from './mixins/_ColorValueInput'
export default {
name: 'color-value-hex',
mixins: [ColorValueInput],
computed: {
hexValue () {
return tinycolor(this.rgb).toHexString()
}
},
methods: {
handleValueInput (val) {
this.updateColor(tinycolor(val).toRgb())
}
}
}
</script>
54 changes: 54 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueHsl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div class="veui-color-value-hsl">
<div class="veui-color-value">
<veui-color-value-input
:value="hsl.h"
:readonly="readonly"
:format="formatHue"
:parse="parseHue"
nudge="hue"
@input="handleHueValueInput"
/>
</div>
<div class="veui-color-value">
<veui-color-value-input
:value="hsl.s"
:readonly="readonly"
:format="formatPercentage"
:parse="parsePercentage"
nudge="percentage"
@input="handleSaturationValueInput"
/>
</div>
<div class="veui-color-value">
<veui-color-value-input
:value="hsl.l"
:readonly="readonly"
:format="formatPercentage"
:parse="parsePercentage"
nudge="percentage"
@input="handleLightnessValueInput"
/>
</div>
</div>
</template>

<script>
import ColorValueInput from './mixins/_ColorValueInput'
export default {
name: 'color-value-hsl',
mixins: [ColorValueInput],
methods: {
handleHueValueInput (val) {
this.updateColor({h: val})
},
handleSaturationValueInput (val) {
this.updateColor({s: val})
},
handleLightnessValueInput (val) {
this.updateColor({l: val})
}
}
}
</script>
117 changes: 117 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

<script>
import Input from '../Input'
import {nudge} from '../../directives'
import {identity} from 'lodash'
export default {
name: 'color-value-input',
components: {
'veui-input': Input
},
directives: {
nudge
},
props: {
value: {
type: [Number, String],
required: true
},
readonly: {
type: Boolean,
default: false
},
format: {
type: Function,
default: identity
},
parse: {
type: Function,
default: identity
},
nudge: {
type: String
}
},
data () {
return {
localValue: null
}
},
computed: {
formattedValue () {
return this.format(this.value)
},
directives () {
if (!this.nudge) {
return []
}
return [
{
name: 'nudge',
value: {
update: this.hanleNudgeUpdate
},
modifiers: {}
}
]
}
},
watch: {
value: {
handler (val) {
this.localValue = this.format(val)
},
immediate: true
}
},
methods: {
hanleNudgeUpdate (increase) {
if (Math.abs(increase) < 1) {
return
}
switch (this.nudge) {
case 'hue':
case 'ff':
break
case 'percentage':
increase /= 100
break
default:
return
}
this.handleValueInput(this.format(this.value + increase))
},
handleValueInput (val) {
this.localValue = val
let realValue
try {
realValue = this.parse(val)
} catch (err) {
return
}
this.$emit('input', realValue)
},
handleValueBlur () {
if (this.formattedValue !== this.localValue) {
this.localValue = this.formattedValue
}
}
},
render () {
return (<veui-input
type="text"
value={this.localValue}
readonly={this.readonly}
onInput={this.handleValueInput}
onBlur={this.handleValueBlur}
{...{ directives: this.directives }}
/>)
}
}
</script>
53 changes: 53 additions & 0 deletions packages/veui/src/components/ColorPicker/_ColorValueRgb.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<div class="veui-color-value-rgb">
<div class="veui-color-value">
<veui-color-value-input
:value="rgb.r"
:readonly="readonly"
:parse="parseFFValue"
nudge="ff"
@input="handleRedValueInput"
/>
</div>

<div class="veui-color-value">
<veui-color-value-input
:value="rgb.g"
:readonly="readonly"
:parse="parseFFValue"
nudge="ff"
@input="handleGreenValueInput"
/>
</div>

<div class="veui-color-value">
<veui-color-value-input
:value="rgb.b"
:readonly="readonly"
:parse="parseFFValue"
nudge="ff"
@input="handleBlueValueInput"
/>
</div>
</div>
</template>

<script>
import ColorValueInput from './mixins/_ColorValueInput'
export default {
name: 'color-value-rgb',
mixins: [ColorValueInput],
methods: {
handleRedValueInput (val) {
this.updateColor({r: val})
},
handleGreenValueInput (val) {
this.updateColor({g: val})
},
handleBlueValueInput (val) {
this.updateColor({b: val})
}
}
}
</script>
55 changes: 55 additions & 0 deletions packages/veui/src/components/ColorPicker/mixins/_ColorHomer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {formatColor} from '../../../utils/color'
import tinycolor from 'tinycolor2'
import {merge} from 'lodash'

export default {
uiTypes: ['color-homer'],
props: {
color: String,
ui: String,
variant: String,
alpha: Boolean,
switchable: Boolean
},
model: {
prop: 'color',
event: 'update:color'
},
data () {
return {
lockedHue: null
}
},
computed: {
hsl () {
let hsl = tinycolor(this.color).toHsl()
return this.lockedHue ? {...hsl, h: this.lockedHue} : hsl
},
hsv () {
let hsv = tinycolor(this.color).toHsv()
return this.lockedHue ? {...hsv, h: this.lockedHue} : hsv
},
rgb () {
return tinycolor(this.color).toRgb()
}
},
methods: {
updateColor (color) {
this.$emit('update:color', formatColor(color, {
format: this.variant
}))
},
updateHsvValue (hsv) {
this.updateColor(merge(this.hsv, hsv))
},
updateHslValue (hsl) {
this.updateColor(merge(this.hsl, hsl))
},
updateRgbValue (rgb) {
this.updateColor(merge(this.rgb, rgb))
},
lockHue (hue) {
this.lockedHue = hue
}
}
}
14 changes: 14 additions & 0 deletions packages/veui/src/components/ColorPicker/mixins/_ColorSlider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ColorUpdater from './_ColorUpdater'
import Slider from '../../Slider'

export default {
components: {
'veui-slider': Slider
},
mixins: [
ColorUpdater
],
props: {
hsl: Object
}
}
26 changes: 26 additions & 0 deletions packages/veui/src/components/ColorPicker/mixins/_ColorUpdater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {getTypedAncestorTracker} from '../../../utils/helper'

export default {
mixins: [
getTypedAncestorTracker('color-homer')
],
methods: {
updateColor ({h, s, v, l, a, r, g, b}) {
switch (true) {
case v !== undefined:
this.colorHomer.updateHsvValue({h, s, v, a})
break

case r !== undefined:
case g !== undefined:
case b !== undefined:
this.colorHomer.updateRgbValue({r, g, b, a})
break

default:
this.colorHomer.updateHslValue({h, s, l, a})
break
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import ColorUpdater from './_ColorUpdater'
import ColorValueInput from '../_ColorValueInput'
import {clamp} from 'lodash'

export default {
components: {
'veui-color-value-input': ColorValueInput
},
mixins: [ColorUpdater],
props: {
hsl: Object,
rgb: Object,
readonly: Boolean
},
methods: {
parseHexValue (val) {
if (!/^#[0-9A-F]{6}$/i.test(val)) {
throw new Error('Illegal hex color value')
}
return val
},

formatHue (val) {
return Math.round(val)
},
parseHue (val) {
let realValue = parseFloat(val)
if (isNaN(realValue)) {
throw new Error('Illegal hue value')
}
return realValue % 360
},

formatPercentage (val) {
return Math.round(val * 100) + '%'
},
parsePercentage (val) {
if (!/^\d+(\.\d+)?%$/.test(val)) {
throw new Error('Illegal percentage value')
}
return clamp(parseFloat(val) / 100, 0, 1)
},

parseFFValue (val) {
let realValue = parseInt(val, 10)
if (isNaN(realValue) || realValue < 0 || realValue > 255) {
throw new Error('Illegal value')
}
return realValue
}
}
}
3 changes: 3 additions & 0 deletions packages/veui/src/index.js
Original file line number Diff line number Diff line change
@@ -53,3 +53,6 @@ export { default as Tooltip } from './components/Tooltip'
export { default as Transfer } from './components/Transfer'
export { default as Tree } from './components/Tree'
export { default as Uploader } from './components/Uploader'
export { default as ColorPicker } from './components/ColorPicker'
export { default as ColorSwatch } from './components/ColorPicker/ColorSwatch'
export { default as ColorPalette } from './components/ColorPicker/ColorPalette'
40 changes: 40 additions & 0 deletions packages/veui/src/utils/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import tinycolor from 'tinycolor2'

export function formatColor (color, {
precision = 4,
format = 'hsl'
} = {}) {
let tcolor = tinycolor(color)
switch (format) {
case 'rgb':
return tcolor.toRgbString()

case 'hex':
return color.a !== 1 ? tcolor.toHex8String() : tcolor.toHexString()

case 'hsl':
default:
// 因为 tinycolor 的 toHslString() 得到的颜色没有小数
// 精度丢失会导致数字修改时突变,所以自己实现一个format保留4位小数
return formatHsla(tcolor.toHsl(), {precision})
}
}

/**
* 格式化 hsla
*
* @param {Number} color.h Hue
* @param {Number} color.s Saturation
* @param {Number} color.l Lightness
* @param {Number} color.a Alpha
* @param {Number} options.precision precision
* @return {String}
*/
export function formatHsla ({h, s, l, a}, {precision = 4} = {}) {
precision = Math.pow(10, precision)
h = Math.round(h % 360 * precision) / precision
s = Math.round(s * 100 * precision) / precision
l = Math.round(l * 100 * precision) / precision
a = Math.round(a * precision) / precision
return a === 1 ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${a})`
}