forked from opentiny/tiny-vue
feat(color-picker): color-picker component (#383)
* feat(color-picker): color-picker component
This commit is contained in:
parent
c1d78fda3b
commit
4c25c74c36
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<tiny-color-picker @confirm="onConfirm" @cancel="onCacnel" alpha />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ColorPicker, Notify } from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyColorPicker: ColorPicker,
|
||||
},
|
||||
setup() {
|
||||
const color = ref('#66ccff')
|
||||
/**
|
||||
* @param {string} hex #rrggbb
|
||||
*/
|
||||
const onConfirm = (hex) => {
|
||||
Notify({
|
||||
type: 'success',
|
||||
position: 'top-right',
|
||||
title: '用户点击了选择',
|
||||
message: hex
|
||||
})
|
||||
}
|
||||
const onCacnel = () => {
|
||||
Notify({
|
||||
type: 'warning',
|
||||
position: 'top-right',
|
||||
title: '用户选择了取消'
|
||||
})
|
||||
}
|
||||
return {
|
||||
color,
|
||||
onConfirm,
|
||||
onCacnel
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-color-picker v-model="color" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import {ColorPicker} from '@opentiny/vue';
|
||||
export default {
|
||||
components: {
|
||||
TinyColorPicker: ColorPicker
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<tiny-color-picker v-model="color" visible />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ColorPicker } from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyColorPicker: ColorPicker
|
||||
},
|
||||
setup() {
|
||||
const color = ref('#66ccff')
|
||||
return {
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<tiny-color-picker v-model="color" />
|
||||
<tiny-button @click="changeColor">
|
||||
切换
|
||||
</tiny-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ColorPicker, Button } from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyColorPicker: ColorPicker,
|
||||
TinyButton: Button
|
||||
},
|
||||
setup() {
|
||||
const color = ref('#66ccff')
|
||||
const changeColor = () => {
|
||||
color.value = color.value === '#66ccff' ? '#000' : '#66ccff'
|
||||
}
|
||||
return {
|
||||
color,
|
||||
changeColor
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<tiny-color-picker @confirm="onConfirm" @cancel="onCacnel" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ColorPicker, Notify } from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyColorPicker: ColorPicker,
|
||||
},
|
||||
setup() {
|
||||
const color = ref('#66ccff')
|
||||
/**
|
||||
* @param {string} hex #rrggbb
|
||||
*/
|
||||
const onConfirm = (hex) => {
|
||||
Notify({
|
||||
type: 'success',
|
||||
position: 'top-right',
|
||||
title: '用户点击了选择',
|
||||
message: hex
|
||||
})
|
||||
}
|
||||
const onCacnel = () => {
|
||||
Notify({
|
||||
type: 'warning',
|
||||
position: 'top-right',
|
||||
title: '用户选择了取消'
|
||||
})
|
||||
}
|
||||
return {
|
||||
color,
|
||||
onConfirm,
|
||||
onCacnel
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: ColorPicker 色彩选择器
|
||||
---
|
||||
|
||||
# ColorPicker 色彩选择器
|
||||
|
||||
<div>ColorPicker 色彩选择器</div>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: ColorPicker
|
||||
---
|
||||
|
||||
# ColorPicker
|
||||
|
||||
<div>ColorPicker</div>
|
|
@ -0,0 +1,103 @@
|
|||
export default {
|
||||
column: '2',
|
||||
owner: '',
|
||||
demos: [
|
||||
{
|
||||
'demoId': 'basic-usage',
|
||||
'name': { 'zh-CN': '基本用法', 'en-US': 'Basic Usage' },
|
||||
'desc': { 'zh-CN': '详细用法参考如下示例', 'en-US': 'For details, see the following example.' },
|
||||
'codeFiles': ['base.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'event',
|
||||
'name': { 'zh-CN': '事件触发', 'en-US': 'event' },
|
||||
'desc': { 'zh-CN': '点击确认是将会触发confirm事件, 取消时触发cancel事件', 'en-US': 'When click confirm will trigger confirm event. When click outside or cancel will trigger cancel event' },
|
||||
'codeFiles': ['event.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'enable-alpha',
|
||||
'name': { 'zh-CN': 'Alpha', 'en-US': 'Alpha' },
|
||||
'desc': { 'zh-CN': 'Alpha选择', 'en-US': 'Alpha select.' },
|
||||
'codeFiles': ['alpha.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'default-visible',
|
||||
'name': { 'zh-CN': '默认显示', 'en-US': 'default-visible' },
|
||||
'desc': {
|
||||
'zh-CN': '当visible为true时, 将会默认显示color-select. visible是响应式的, 所以你可以强制控制color-select的状态。当visible切换的时候, 会触发cancel事件',
|
||||
'en-US': 'If visible is true the <code>color-select</code> will show. The visible prop is reactive so you can force change <code>color-select</code> show or not. When change visible will trigger cancel event'
|
||||
},
|
||||
'codeFiles': ['default-visible.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'dynamic-color-change',
|
||||
'name': { 'zh-CN': '颜色动态切换', 'en-US': 'dynamic-color-change' },
|
||||
'desc': {
|
||||
'zh-CN': '可以动态切换color属性, 以满足各种需求',
|
||||
'en-US': 'Can dynamically switch color attributes to meet various needs'
|
||||
},
|
||||
'codeFiles': ['dynamic-color-change.vue']
|
||||
},
|
||||
],
|
||||
apis: [
|
||||
{
|
||||
'name': 'color-picker',
|
||||
'type': 'component',
|
||||
'properties': [
|
||||
{
|
||||
'name': 'modelValue',
|
||||
'type': 'String',
|
||||
'defaultValue': 'transparent',
|
||||
desc: {
|
||||
'zh-CN': '默认颜色',
|
||||
'en-US': 'default color'
|
||||
},
|
||||
demoId: 'basic-usage'
|
||||
},
|
||||
{
|
||||
name: 'visible',
|
||||
type: 'boolean',
|
||||
defaultValue: 'false',
|
||||
desc: {
|
||||
'zh-CN': '是否默认显示color-select',
|
||||
'en-US': 'Is color select displayed by default'
|
||||
},
|
||||
demoId: 'default-visible'
|
||||
},
|
||||
{
|
||||
name: 'alpha',
|
||||
type: 'boolean',
|
||||
defaultValue: 'false',
|
||||
desc: {
|
||||
'zh-CN': '是否启用alpha选择',
|
||||
'en-US': 'enable alpha select or not'
|
||||
},
|
||||
demoId: 'enable-alpha'
|
||||
}
|
||||
],
|
||||
'events': [
|
||||
{
|
||||
name: 'confirm',
|
||||
type: '(hex:string) => void',
|
||||
defaultValue: '',
|
||||
desc: {
|
||||
'zh-CN': '按下确认时触发该事件',
|
||||
'en-US': 'When click confirm will trigger confirm event'
|
||||
},
|
||||
demoId: 'event'
|
||||
},
|
||||
{
|
||||
name: 'cancel',
|
||||
type: '()=>void',
|
||||
defaultValue: '',
|
||||
desc: {
|
||||
'zh-CN': '按下取消或点击外部的时触发该事件',
|
||||
'en-US': 'When click cancel or click out-side will trigger cancel event'
|
||||
},
|
||||
demoId: 'event'
|
||||
}
|
||||
],
|
||||
'slots': []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -106,7 +106,8 @@ export const cmpMenus = [
|
|||
{ 'nameCn': '滑块', 'name': 'Slider', 'key': 'slider' },
|
||||
{ 'nameCn': '开关', 'name': 'Switch', 'key': 'switch' },
|
||||
{ 'nameCn': '时间选择器', 'name': 'TimePicker', 'key': 'time-picker' },
|
||||
{ 'nameCn': '时间选择', 'name': 'TimeSelect', 'key': 'time-select' }
|
||||
{ 'nameCn': '时间选择', 'name': 'TimeSelect', 'key': 'time-select' },
|
||||
{ 'nameCn': '颜色选择器', 'name': 'ColorPicker', 'key': 'color-picker' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
10
package.json
10
package.json
|
@ -112,6 +112,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vue/composition-api": "1.2.2",
|
||||
"color": "^4.2.3",
|
||||
"cropperjs": "1.5.12",
|
||||
"crypto-js": "4.1.1",
|
||||
"echarts": "5.4.1",
|
||||
|
@ -125,6 +126,7 @@
|
|||
"@antfu/eslint-config": "^0.38.6",
|
||||
"@commitlint/cli": "^17.3.0",
|
||||
"@commitlint/config-conventional": "^17.3.0",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/eslint": "^8.4.10",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/shelljs": "^0.8.12",
|
||||
|
@ -135,19 +137,19 @@
|
|||
"@vue/tsconfig": "^0.4.0",
|
||||
"depcheck": "1.4.3",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fs-extra": "^11.1.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^7.0.1",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-less": "^5.0.0",
|
||||
"gulp-svg-inline": "^1.0.1",
|
||||
"gulp-transform": "^3.0.5",
|
||||
"minimist": "^1.2.8",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"lerna": "^6.4.0",
|
||||
"lint-staged": "^13.0.3",
|
||||
"minimist": "^1.2.8",
|
||||
"node-xlsx": "^0.21.0",
|
||||
"nx": "^15.4.5",
|
||||
"prettier": "^3.0.0",
|
||||
|
|
|
@ -602,6 +602,11 @@
|
|||
"pc"
|
||||
]
|
||||
},
|
||||
"ColorPicker": {
|
||||
"path": "vue/src/color-picker/index.ts",
|
||||
"type": "component",
|
||||
"exclude": false
|
||||
},
|
||||
"ColumnListGroup": {
|
||||
"path": "vue/src/column-list-group/index.ts",
|
||||
"type": "component",
|
||||
|
@ -2676,4 +2681,4 @@
|
|||
"pc"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import type { IColorPickerRef } from "@/types";
|
||||
|
||||
export const calcLeftByAlpha = (wrapper: HTMLElement, thumb: HTMLElement, alpha: number) => {
|
||||
return Math.round(
|
||||
(alpha * (wrapper.offsetWidth - thumb.offsetWidth / 2)) / 100
|
||||
)
|
||||
}
|
||||
|
||||
export const updateThumb = (alpha: number, thumb: HTMLElement, wrapper: HTMLElement) => {
|
||||
thumb.style.left = `${calcLeftByAlpha(wrapper, thumb, alpha)}px`
|
||||
}
|
||||
|
||||
export const onDrag = (
|
||||
event: MouseEvent, bar: IColorPickerRef<HTMLElement>, thumb: IColorPickerRef<HTMLElement>,
|
||||
alpha: IColorPickerRef<number>
|
||||
) => {
|
||||
const rect = bar.value.getBoundingClientRect()
|
||||
const { clientX } = event
|
||||
let left = clientX - rect.left
|
||||
left = Math.max(thumb.value.offsetWidth / 2, left)
|
||||
left = Math.min(left, rect.width - thumb.value.offsetWidth / 2)
|
||||
alpha.value = Math.round(
|
||||
((left - thumb.value.offsetWidth / 2) / (rect.width - thumb.value.offsetWidth)) * 100
|
||||
)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import {IColorPickerRef as Ref} from '@/types';
|
||||
import Color from '../utils/color'
|
||||
import { draggable } from '../utils/use-drag'
|
||||
import { onDrag, updateThumb } from '.'
|
||||
|
||||
export const api = ['state', 'color', 'slider', 'alphaWrapper', 'alphaThumb']
|
||||
|
||||
export const renderless = (props, context, { emit }) => {
|
||||
const hex = props.color
|
||||
const color = new Color(hex, props.alpha)
|
||||
const [rr, gg, bb] = color.getRGB()
|
||||
const r = context.ref(rr)
|
||||
const g = context.ref(gg)
|
||||
const b = context.ref(bb)
|
||||
const slider: Ref<HTMLElement> = context.ref()
|
||||
const alphaWrapper: Ref<HTMLElement> = context.ref()
|
||||
const alphaThumb: Ref<HTMLElement> = context.ref()
|
||||
const alpha = context.ref(color.get('a'))
|
||||
context.watch(() => props.color, (hex: string) => {
|
||||
color.reset(hex)
|
||||
const [rr, gg, bb] = color.getRGB()
|
||||
r.value = rr
|
||||
g.value = gg
|
||||
b.value = bb
|
||||
})
|
||||
context.watch(alpha, (newAlpha) => {
|
||||
updateThumb(newAlpha, alphaThumb.value, alphaWrapper.value)
|
||||
emit('alpha-update', alpha.value)
|
||||
})
|
||||
const background = context.computed(() => {
|
||||
return `linear-gradient(to right, rgba(${r.value}, ${g.value}, ${b.value}, 0) 0%, rgba(${r.value}, ${g.value}, ${b.value}, 1) 100%)`
|
||||
})
|
||||
const state = context.reactive({
|
||||
background,
|
||||
hex
|
||||
})
|
||||
const api = {
|
||||
state,
|
||||
color: props.color,
|
||||
slider,
|
||||
alphaWrapper,
|
||||
alphaThumb,
|
||||
}
|
||||
context.onMounted(() => {
|
||||
updateThumb(alpha.value, alphaThumb.value, slider.value)
|
||||
draggable(slider.value, {
|
||||
drag(event) {
|
||||
onDrag(event as MouseEvent, slider, alphaThumb, alpha)
|
||||
}
|
||||
})
|
||||
})
|
||||
return api
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { IColorPickerRef } from '@/types'
|
||||
import type Color from '../utils/color'
|
||||
|
||||
export const setPosition = (el: HTMLElement, x: number, y: number) => {
|
||||
el.style.top = `${y}px`
|
||||
el.style.left = `${x}px`
|
||||
}
|
||||
export const getXBySaturation = (s: number, width: number) => (s * width) / 100
|
||||
export const getYByLight = (l: number, height: number) => ((100 - l) * height) / 100
|
||||
export const updatePosition = (event: MouseEvent | TouchEvent, rect: DOMRect, cursor: IColorPickerRef<HTMLElement>) => {
|
||||
let x = (event as MouseEvent).clientX - rect.left
|
||||
let y = (event as MouseEvent).clientY - rect.top
|
||||
x = Math.max(0, x)
|
||||
x = Math.min(x, rect.width)
|
||||
y = Math.max(0, y)
|
||||
y = Math.min(y, rect.height)
|
||||
|
||||
setPosition(cursor.value, x - 1 / 2 * cursor.value.offsetWidth, y - 1 / 2 * cursor.value.offsetWidth)
|
||||
return { x, y }
|
||||
}
|
||||
export const calcSaturation = (x: number, width: number) => (x / width)
|
||||
export const calcBrightness = (y: number, height: number) => 100 - (y / height) * 100
|
||||
export const getThumbTop = (wrapper: HTMLElement, thumb: HTMLElement, hue: number) => {
|
||||
return Math.round(
|
||||
(hue * (wrapper.offsetHeight - thumb.offsetHeight / 2)) / 360
|
||||
)
|
||||
}
|
||||
|
||||
export const resetCursor = (
|
||||
s: number, v: number,
|
||||
wrapper: IColorPickerRef<HTMLElement>,
|
||||
cursor: IColorPickerRef<HTMLElement>,
|
||||
thumb: IColorPickerRef<HTMLElement>,
|
||||
color: Color, h: IColorPickerRef<number>
|
||||
) => {
|
||||
const { width, height } = wrapper.value.getBoundingClientRect()
|
||||
const x = getXBySaturation(s, width) - 1 / 2 * cursor.value.offsetWidth
|
||||
const y = getYByLight(v, height) - 1 / 2 * cursor.value.offsetWidth
|
||||
setPosition(cursor.value, x < 0 ? 0 : x, y < 0 ? 0 : y)
|
||||
const thummbTop = getThumbTop(wrapper.value, thumb.value, color.get('h'))
|
||||
thumb.value.style.top = `${thummbTop}px`
|
||||
h.value = color.get('h')
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import {IColorPickerRef as Ref} from '@/types';
|
||||
import { draggable } from '../utils/use-drag'
|
||||
import Color from '../utils/color'
|
||||
import {
|
||||
calcBrightness,
|
||||
calcSaturation,
|
||||
updatePosition,
|
||||
getThumbTop,
|
||||
resetCursor,
|
||||
} from './index'
|
||||
|
||||
export const api = ['state', 'cursor', 'wrapper', 'bar', 'thumb']
|
||||
export const renderless = (props, context, { emit }) => {
|
||||
const cursor: Ref<HTMLElement> = context.ref()
|
||||
const wrapper: Ref<HTMLElement> = context.ref()
|
||||
const thumb: Ref<HTMLElement> = context.ref()
|
||||
const bar: Ref<HTMLElement> = context.ref()
|
||||
const color = new Color(props.color)
|
||||
const h = context.ref(color.get('h'))
|
||||
|
||||
const background = context.computed(() => {
|
||||
return `hsl(${h.value}deg, 100%, 50%)`
|
||||
})
|
||||
const state = context.reactive({
|
||||
background
|
||||
})
|
||||
const api = { state, cursor, wrapper, bar, thumb }
|
||||
context.watch(() => props.color, (newColor) => {
|
||||
color.reset(newColor)
|
||||
resetCursor(color.get('s'), color.get('v'), wrapper, cursor, thumb, color, h)
|
||||
})
|
||||
context.onMounted(() => {
|
||||
const thumbTop = getThumbTop(wrapper.value, thumb.value, h.value)
|
||||
thumb.value.style.top = `${thumbTop}px`
|
||||
resetCursor(
|
||||
color.get('s'),
|
||||
color.get('v'),
|
||||
wrapper,
|
||||
cursor,
|
||||
thumb,
|
||||
color,
|
||||
h
|
||||
)
|
||||
draggable(wrapper.value, {
|
||||
drag(event) {
|
||||
const rect = wrapper.value.getBoundingClientRect()
|
||||
const { x, y } = updatePosition(event, rect, cursor)
|
||||
color.set({
|
||||
s: calcSaturation(x, rect.width) * 100,
|
||||
v: calcBrightness(y, rect.height)
|
||||
})
|
||||
emit('sv-update', {
|
||||
s: color.get('s'),
|
||||
v: color.get('v')
|
||||
})
|
||||
},
|
||||
})
|
||||
draggable(bar.value, {
|
||||
drag(event) {
|
||||
const e = event as MouseEvent
|
||||
const rect = bar.value.getBoundingClientRect()
|
||||
let top = e.clientY - rect.top
|
||||
top = Math.min(top, rect.height - thumb.value.offsetHeight / 2)
|
||||
top = Math.max(thumb.value.offsetHeight / 2, top)
|
||||
thumb.value.style.top = `${top}px`
|
||||
h.value = Math.round(
|
||||
((top - thumb.value.offsetHeight / 2) /
|
||||
(rect.height - thumb.value.offsetHeight)) *
|
||||
360
|
||||
)
|
||||
emit('hue-update', h.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
return api
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import {IColorPickerRef} from '@/types';
|
||||
import type Color from './utils/color'
|
||||
|
||||
export const onConfirm = (
|
||||
hex: IColorPickerRef<string>, triggerBg: IColorPickerRef<string>,
|
||||
res: IColorPickerRef<string>, emit, isShow: IColorPickerRef<boolean>
|
||||
) => {
|
||||
return () => {
|
||||
hex.value = res.value
|
||||
triggerBg.value = res.value
|
||||
emit('confirm', res.value)
|
||||
isShow.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export const onCancel = (
|
||||
res: IColorPickerRef<string>, triggerBg: IColorPickerRef<string>, emit, isShow: IColorPickerRef<boolean>
|
||||
) => {
|
||||
return () => {
|
||||
res.value = triggerBg.value
|
||||
if (isShow.value){
|
||||
emit('cancel')
|
||||
}
|
||||
isShow.value = false
|
||||
}
|
||||
}
|
||||
export const onColorUpdate = (color: Color, res: IColorPickerRef<string>) => {
|
||||
res.value = color.getHex()
|
||||
}
|
||||
|
||||
export const onHSVUpdate = (color: Color, res: IColorPickerRef<string>, hex: IColorPickerRef<string>) => {
|
||||
return {
|
||||
onHueUpdate: (hue: number) => {
|
||||
color.set({ h: hue })
|
||||
onColorUpdate(color, res)
|
||||
hex.value = color.getHex()
|
||||
},
|
||||
onSVUpdate: ({ s, v }: { s: number; v: number }) => {
|
||||
color.set({ s, v })
|
||||
onColorUpdate(color, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const onAlphaUpdate = (color: Color, res: IColorPickerRef<string>) => {
|
||||
return {
|
||||
update: (alpha: number) => {
|
||||
color.set({ a: alpha })
|
||||
onColorUpdate(color, res)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { hsv, rgb } from 'color'
|
||||
|
||||
function hexToRgb(hex: string) {
|
||||
let r = parseInt(hex.substring(1, 3), 16)
|
||||
let g = parseInt(hex.substring(3, 5), 16)
|
||||
let b = parseInt(hex.substring(5, 7), 16)
|
||||
let a = parseInt(hex.slice(7), 16) / 255
|
||||
return { r, g, b, a: a * 100 }
|
||||
}
|
||||
const normalizeHexColor = (color: string) => {
|
||||
let normalizedColor: string = color.replace('#', '')
|
||||
if (normalizedColor.length === 3) {
|
||||
normalizedColor = normalizedColor.split('').map(char => char + char).join('')
|
||||
}
|
||||
normalizedColor = normalizedColor.padEnd(6, '0')
|
||||
|
||||
const r = parseInt(normalizedColor.substr(0, 2), 16)
|
||||
const g = parseInt(normalizedColor.substr(2, 2), 16)
|
||||
const b = parseInt(normalizedColor.substr(4, 2), 16)
|
||||
let a = 255
|
||||
if (normalizedColor.length === 8) {
|
||||
a = parseInt(normalizedColor.slice(6), 16)
|
||||
}
|
||||
|
||||
const hexR = ('0' + r.toString(16)).slice(-2)
|
||||
const hexG = ('0' + g.toString(16)).slice(-2)
|
||||
const hexB = ('0' + b.toString(16)).slice(-2)
|
||||
const alpha = ('0' + a.toString(16)).slice(-2)
|
||||
|
||||
return `#${hexR}${hexG}${hexB}${alpha}`
|
||||
}
|
||||
|
||||
export type Format = 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
export default class Color {
|
||||
private hex = '#000'
|
||||
private h = 0
|
||||
private s = 0
|
||||
private v = 0
|
||||
private a = 100
|
||||
private enableAlpha = false
|
||||
constructor(value: string, alpha = false) {
|
||||
this.reset(value)
|
||||
this.enableAlpha = alpha
|
||||
}
|
||||
|
||||
reset(hex: string) {
|
||||
if (this.hex === 'transparent') {
|
||||
this.h = 0
|
||||
this.s = 0
|
||||
this.v = 0
|
||||
this.a = 0
|
||||
return
|
||||
}
|
||||
this.hex = normalizeHexColor(hex)
|
||||
const { r, g, b, a } = hexToRgb(this.hex)
|
||||
const { h, s, v } = rgb([r, g, b, a]).hsv().object()
|
||||
this.h = h
|
||||
this.s = s
|
||||
this.v = v
|
||||
this.a = a
|
||||
}
|
||||
|
||||
set({ h, s, v, a }: { h?: number; s?: number; v?: number; a?: number }) {
|
||||
this.h = h ?? this.h
|
||||
this.s = s ?? this.s
|
||||
this.v = v ?? this.v
|
||||
this.a = a ?? this.a
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns [R,G,B]
|
||||
*/
|
||||
getRGB() {
|
||||
return hsv(this.h, this.s, this.v).rgb().array()
|
||||
}
|
||||
|
||||
getHex() {
|
||||
if (!this.enableAlpha) {
|
||||
return hsv(this.h, this.s, this.v).hex().toString()
|
||||
}
|
||||
return hsv(this.h, this.s, this.v, this.a / 100).hexa().toString()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns [h,s,l]
|
||||
*/
|
||||
getHSL() {
|
||||
return hsv(this.h, this.s, this.v).hsl().unitArray()
|
||||
}
|
||||
|
||||
getHSV() {
|
||||
return {
|
||||
h: this.h,
|
||||
s: this.s,
|
||||
v: this.v,
|
||||
a: this.a
|
||||
}
|
||||
}
|
||||
|
||||
get(key: 'h' | 's' | 'v' | 'a') {
|
||||
return this[key]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
let isDragging = false
|
||||
|
||||
export interface DraggableOptions {
|
||||
drag?: (event: MouseEvent | TouchEvent) => void
|
||||
start?: (event: MouseEvent | TouchEvent) => void
|
||||
end?: (event: MouseEvent | TouchEvent) => void
|
||||
}
|
||||
|
||||
export function draggable(element: HTMLElement, options: DraggableOptions) {
|
||||
const moveFn = function (event: MouseEvent | TouchEvent) {
|
||||
options.drag?.(event)
|
||||
}
|
||||
|
||||
const upFn = function (event: MouseEvent | TouchEvent) {
|
||||
document.removeEventListener('mousemove', moveFn)
|
||||
document.removeEventListener('mouseup', upFn)
|
||||
document.removeEventListener('touchmove', moveFn)
|
||||
document.removeEventListener('touchend', upFn)
|
||||
document.onselectstart = null
|
||||
document.ondragstart = null
|
||||
|
||||
isDragging = false
|
||||
|
||||
options.end?.(event)
|
||||
}
|
||||
|
||||
const downFn = function (event: MouseEvent | TouchEvent) {
|
||||
if (isDragging) return
|
||||
event.preventDefault()
|
||||
document.onselectstart = () => false
|
||||
document.ondragstart = () => false
|
||||
document.addEventListener('mousemove', moveFn)
|
||||
document.addEventListener('mouseup', upFn)
|
||||
document.addEventListener('touchmove', moveFn)
|
||||
document.addEventListener('touchend', upFn)
|
||||
|
||||
isDragging = true
|
||||
|
||||
options.start?.(event)
|
||||
}
|
||||
element.addEventListener('mousedown', downFn)
|
||||
element.addEventListener('touchstart', downFn)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import {IColorPickerRef as Ref} from '@/types';
|
||||
import Color from './utils/color'
|
||||
import { onConfirm, onCancel, onHSVUpdate, onAlphaUpdate } from '.'
|
||||
|
||||
export const api = [
|
||||
'state',
|
||||
'changeVisible',
|
||||
'cursor',
|
||||
'onColorUpdate',
|
||||
'onHueUpdate',
|
||||
'onSVUpdate',
|
||||
'onConfirm',
|
||||
'onCancel',
|
||||
'onAlphaUpdate',
|
||||
'alpha'
|
||||
]
|
||||
|
||||
export const renderless = (
|
||||
props,
|
||||
context,
|
||||
{ emit }
|
||||
) => {
|
||||
const { modelValue, visible } = context.toRefs(props)
|
||||
const hex = context.ref(modelValue.value ?? 'transparent')
|
||||
const res = context.ref(modelValue.value ?? 'transparent')
|
||||
const triggerBg = context.ref(modelValue.value ?? 'transparent')
|
||||
const isShow = context.ref(visible?.value ?? false)
|
||||
const cursor: Ref<HTMLElement> = context.ref()
|
||||
const changeVisible = (state: boolean) => {
|
||||
isShow.value = state
|
||||
}
|
||||
const color = new Color(hex.value, props.alpha)
|
||||
const state = context.reactive({
|
||||
isShow,
|
||||
hex,
|
||||
color,
|
||||
triggerBg,
|
||||
defaultValue: modelValue,
|
||||
res
|
||||
})
|
||||
context.watch(modelValue, (newValue) => {
|
||||
hex.value = newValue
|
||||
res.value = newValue
|
||||
triggerBg.value = newValue
|
||||
color.reset(hex.value)
|
||||
})
|
||||
context.watch(visible, (visible) => {
|
||||
isShow.value = visible
|
||||
})
|
||||
const { onHueUpdate, onSVUpdate } = onHSVUpdate(color, res, hex)
|
||||
const { update } = onAlphaUpdate(color, res)
|
||||
const api = {
|
||||
state,
|
||||
changeVisible,
|
||||
onHueUpdate,
|
||||
onSVUpdate,
|
||||
onConfirm: onConfirm(hex, triggerBg, res, emit, isShow),
|
||||
onCancel: onCancel(res, triggerBg, emit, isShow),
|
||||
onAlphaUpdate: update,
|
||||
cursor,
|
||||
alpha: props.alpha
|
||||
}
|
||||
return api
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export type IColorPickerRef<T> = {value: T}
|
|
@ -203,3 +203,4 @@ export * from './wheel.type'
|
|||
export * from './wizard.type'
|
||||
export * from './year-range.type'
|
||||
export * from './year-table.type'
|
||||
export * from './color-picker.type'
|
|
@ -0,0 +1,129 @@
|
|||
@import '../custom.less';
|
||||
@import './vars.less';
|
||||
|
||||
@colorPickerPrefix: ~'@{css-prefix}color-picker';
|
||||
|
||||
.@{colorPickerPrefix} {
|
||||
.component-css-vars-colorpicker();
|
||||
|
||||
&__trigger {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--ti-color-picker-border-radius-sm);
|
||||
border: var(--ti-color-picker-border-weight) solid var(--ti-color-picker-border-color);
|
||||
box-sizing: content-box;
|
||||
padding: var(--ti-color-picker-spacing);
|
||||
cursor: pointer;
|
||||
.component-css-vars-colorpicker();
|
||||
|
||||
.@{colorPickerPrefix}__inner {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--ti-color-picker-border-radius-sm);
|
||||
background: var(--ti-color-picker-background);
|
||||
}
|
||||
|
||||
.@{colorPickerPrefix}__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
max-width: 300px;
|
||||
z-index: var(--ti-color-picker__select__wrapper-zindex);
|
||||
margin-top: var(--ti-color-picker-spacing);
|
||||
background: var(--ti-color-picker__wrapper-bg);
|
||||
gap: var(--ti-color-picker-spacing);
|
||||
padding: var(--ti-color-picker-spacing-2x);
|
||||
box-shadow: var(--ti-color-picker-shadow);
|
||||
|
||||
&__tools {
|
||||
display: flex;
|
||||
|
||||
.tiny-input {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.tiny-button-group {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
&__color-select {
|
||||
width: 280px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
|
||||
.white {
|
||||
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.black {
|
||||
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
.white,
|
||||
.black {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100%;
|
||||
border: 1px solid white;
|
||||
background: rgba(0, 0, 0, .15);
|
||||
box-shadow: inset 0 0 1px 1px #0000004d, 0 0 1px 2px #0006;
|
||||
}
|
||||
}
|
||||
|
||||
&__hue-select {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
border-radius: var(--ti-color-picker-border-radius-xs);
|
||||
background: linear-gradient(to bottom, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__alpha {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-radius: var(--ti-color-picker-border-radius-xs);
|
||||
margin-top: var(--ti-color-picker-spacing-2x);
|
||||
&__slider {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.component-css-vars-colorpicker() {
|
||||
--ti-color-picker-background: var(--ti-common-color-transparent);
|
||||
--ti-color-picker-border-color: var(--ti-base-color-common-2);
|
||||
--ti-color-picker-border-weight: var(--ti-common-border-weight-normal);
|
||||
--ti-color-picker-border-radius-xs: var(--ti-common-border-radius-normal);
|
||||
--ti-color-picker-border-radius-sm: var(--ti-common-border-radius-1);
|
||||
--ti-color-picker-border-radius: var(--ti-common-border-radius-2);
|
||||
--ti-color-picker-spacing: var(--ti-common-space-base);
|
||||
--ti-color-picker-spacing-2x: var(--ti-common-space-2x);
|
||||
--ti-color-picker-shadow: var(--ti-common-shadow-2-down);
|
||||
--ti-color-picker__wrapper-zindex: 1000;
|
||||
--ti-color-picker__wrapper-bg: var(--ti-common-color-bg-white-emphasize);
|
||||
}
|
|
@ -82,6 +82,10 @@ export default {
|
|||
total: 'Total',
|
||||
value: 'Value'
|
||||
},
|
||||
colorPicker: {
|
||||
confirm: 'Ok',
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
creditCardForm: {
|
||||
submit: 'Submit'
|
||||
},
|
||||
|
|
|
@ -82,6 +82,10 @@ export default {
|
|||
total: '总计',
|
||||
value: '数值'
|
||||
},
|
||||
colorPicker: {
|
||||
confirm: '选择',
|
||||
cancel: '取消'
|
||||
},
|
||||
creditCardForm: {
|
||||
submit: '提交'
|
||||
},
|
||||
|
|
|
@ -39,6 +39,7 @@ import Checkbox from '@opentiny/vue-checkbox'
|
|||
import CheckboxGroup from '@opentiny/vue-checkbox-group'
|
||||
import Collapse from '@opentiny/vue-collapse'
|
||||
import CollapseItem from '@opentiny/vue-collapse-item'
|
||||
import ColorPicker from '@opentiny/vue-color-picker'
|
||||
import ColumnListGroup from '@opentiny/vue-column-list-group'
|
||||
import ColumnListItem from '@opentiny/vue-column-list-item'
|
||||
import ConfigProvider from '@opentiny/vue-config-provider'
|
||||
|
@ -143,6 +144,7 @@ const components = [
|
|||
CheckboxGroup,
|
||||
Collapse,
|
||||
CollapseItem,
|
||||
ColorPicker,
|
||||
ColumnListGroup,
|
||||
ColumnListItem,
|
||||
ConfigProvider,
|
||||
|
@ -269,6 +271,7 @@ export {
|
|||
CheckboxGroup,
|
||||
Collapse,
|
||||
CollapseItem,
|
||||
ColorPicker,
|
||||
ColumnListGroup,
|
||||
ColumnListItem,
|
||||
ConfigProvider,
|
||||
|
@ -373,6 +376,7 @@ export default {
|
|||
CheckboxGroup,
|
||||
Collapse,
|
||||
CollapseItem,
|
||||
ColorPicker,
|
||||
ColumnListGroup,
|
||||
ColumnListItem,
|
||||
ConfigProvider,
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
"@opentiny/vue-collapse": "workspace:~",
|
||||
"@opentiny/vue-collapse-item": "workspace:~",
|
||||
"@opentiny/vue-collapse-transition": "workspace:~",
|
||||
"@opentiny/vue-color-picker": "workspace:~",
|
||||
"@opentiny/vue-column-list-group": "workspace:~",
|
||||
"@opentiny/vue-column-list-item": "workspace:~",
|
||||
"@opentiny/vue-company": "workspace:~",
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { mountPcMode } from '@opentiny-internal/vue-test-utils'
|
||||
import { expect, test, describe } from 'vitest'
|
||||
import ColorPicker from '@opentiny/vue-color-picker'
|
||||
|
||||
describe('PC Mode', () => {
|
||||
const mount = mountPcMode
|
||||
describe('default color', () => {
|
||||
test('static', () => {
|
||||
const wrapper = mount(ColorPicker, {
|
||||
props: {
|
||||
modelValue: '#66ccff',
|
||||
}
|
||||
})
|
||||
expect(wrapper.classes()).toContain('tiny-color-picker__trigger')
|
||||
expect(wrapper.find('div .tiny-color-picker__inner').attributes().style).toContain('102, 204, 255')
|
||||
})
|
||||
test('dynmaic', async () => {
|
||||
const wrapper = mount(ColorPicker, {
|
||||
props: {
|
||||
modelValue: '#66ccff',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('div .tiny-color-picker__inner').attributes().style).toContain('102, 204, 255')
|
||||
await wrapper.setProps({ modelValue: '#000' })
|
||||
expect(wrapper.find('div .tiny-color-picker__inner').attributes().style).not.toContain('102, 204, 255')
|
||||
})
|
||||
})
|
||||
test('should show color-select wrapper when visible is true', () => {
|
||||
const wrapper = mount(ColorPicker, {
|
||||
props: {
|
||||
modelValue: '#66ccff',
|
||||
visible: true
|
||||
},
|
||||
})
|
||||
expect(wrapper.findAll('button').length).not.toBe(0)
|
||||
test('should hidden when click trigger, even if visible is true', async () => {
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
})
|
||||
test('should show color-select wrapper when click', async () => {
|
||||
const wrapper = mount(ColorPicker, {
|
||||
props: {
|
||||
modelValue: '#66ccff'
|
||||
},
|
||||
})
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.findAll('button').length).not.toBe(0)
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
test('should not be throw when v-model is undefined', () => {
|
||||
const wrapper = mount(ColorPicker, {
|
||||
props: {
|
||||
visible: false
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('div .tiny-color-picker__inner').attributes().style).toContain('transparent')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import ColorPicker from './src/index'
|
||||
import '@opentiny/vue-theme/color-picker/index.less'
|
||||
import { version } from './package.json'
|
||||
|
||||
ColorPicker.model = {
|
||||
prop: 'modelValue',
|
||||
event: 'update:modelValue'
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
ColorPicker.install = function (Vue) {
|
||||
Vue.component(ColorPicker.name, ColorPicker)
|
||||
}
|
||||
|
||||
ColorPicker.version = version
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (process.env.BUILD_TARGET === 'runtime') {
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
ColorPicker.install(window.Vue)
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorPicker
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@opentiny/vue-color-picker",
|
||||
"version": "5.8.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"module": "index.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@opentiny/vue-common": "workspace:~",
|
||||
"@opentiny/vue-renderless": "workspace:~",
|
||||
"@opentiny/vue-input": "workspace:~",
|
||||
"@opentiny/vue-option": "workspace:~",
|
||||
"@opentiny/vue-button": "workspace:~",
|
||||
"@opentiny/vue-locale": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentiny-internal/vue-test-utils": "workspace:*",
|
||||
"vitest": "^0.31.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<div class="tiny-color-picker__wrapper__alpha" ref="alphaWrapper">
|
||||
<div
|
||||
class="tiny-color-picker__wrapper__alpha__slider" :style="{
|
||||
background: state.background
|
||||
}"
|
||||
ref="slider"
|
||||
></div>
|
||||
<div
|
||||
class="tiny-color-picker__wrapper__alpha__thumb" :style="{
|
||||
top: 0,
|
||||
left: 0
|
||||
}"
|
||||
ref="alphaThumb"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, setup } from '@opentiny/vue-common'
|
||||
import { renderless, api } from '@opentiny/vue-renderless/color-picker/alpha-select/vue'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['alpha-update'],
|
||||
props: {
|
||||
color: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
},
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="tiny-color-picker__wrapper__inner">
|
||||
<div
|
||||
class="tiny-color-picker__wrapper__inner__color-select" ref="wrapper" :style="{
|
||||
background: state.background,
|
||||
}"
|
||||
>
|
||||
<div class="white"></div>
|
||||
<div class="black"></div>
|
||||
<div class="cursor" ref="cursor"></div>
|
||||
</div>
|
||||
<div class="tiny-color-picker__wrapper__inner__hue-select" ref="bar">
|
||||
<div ref="thumb"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, setup } from '@opentiny/vue-common'
|
||||
import { renderless, api } from '@opentiny/vue-renderless/color-picker/color-select/vue'
|
||||
import '@opentiny/vue-theme/color-picker/index.less'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['hue-update', 'sv-update'],
|
||||
props: {
|
||||
color: {
|
||||
type: String
|
||||
},
|
||||
alpha: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
},
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,21 @@
|
|||
import { $props, $setup, $prefix, defineComponent } from '@opentiny/vue-common'
|
||||
import template from 'virtual-template?pc|mobile'
|
||||
|
||||
const $constants = {}
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'ColorPicker',
|
||||
props: {
|
||||
...$props,
|
||||
_constants: {
|
||||
type: Object,
|
||||
default: () => $constants
|
||||
},
|
||||
modelValue: String,
|
||||
visible: Boolean,
|
||||
alpha: Boolean
|
||||
},
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderless, api } from '@opentiny/vue-renderless/color-picker/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['update:modelValue'],
|
||||
props: [...props, 'modelValue'],
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="tiny-color-picker__trigger" v-clickoutside="onCancel" @click="() => changeVisible(!state.isShow)">
|
||||
<div
|
||||
class="tiny-color-picker__inner" :style="{
|
||||
background: state.triggerBg ?? ''
|
||||
}"
|
||||
>
|
||||
<IconChevronDown />
|
||||
</div>
|
||||
<Transition name="tiny-zoom-in-top">
|
||||
<div class="tiny-color-picker__wrapper" @click.stop v-if="state.isShow">
|
||||
<color-select
|
||||
@hue-update="onHueUpdate"
|
||||
@sv-update="onSVUpdate"
|
||||
:color="state.hex"
|
||||
/>
|
||||
<alpha-select :color="state.res" @alpha-update="onAlphaUpdate" v-if="alpha" />
|
||||
<div class="tiny-color-picker__wrapper__tools">
|
||||
<tiny-input v-model="state.res" />
|
||||
<tiny-button-group>
|
||||
<tiny-button type="text" @click="onCancel">
|
||||
{{ t('ui.colorPicker.cancel') }}
|
||||
</tiny-button>
|
||||
<tiny-button @click="onConfirm">
|
||||
{{ t('ui.colorPicker.confirm') }}
|
||||
</tiny-button>
|
||||
</tiny-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderless, api } from '@opentiny/vue-renderless/color-picker/vue'
|
||||
import { props, setup, defineComponent, directive } from '@opentiny/vue-common'
|
||||
import { IconChevronDown } from '@opentiny/vue-icon'
|
||||
import colorSelect from './components/color-select.vue'
|
||||
import alphaSelect from './components/alpha-select.vue'
|
||||
import Button from '@opentiny/vue-button'
|
||||
import ButtonGroup from '@opentiny/vue-button-group'
|
||||
import Input from '@opentiny/vue-input'
|
||||
import Clickoutside from '@opentiny/vue-renderless/common/deps/clickoutside'
|
||||
import '@opentiny/vue-theme/color-picker/index.less'
|
||||
import { language } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['update:modelValue', 'confirm', 'cancel'],
|
||||
props: [...props, 'modelValue', 'visible', 'alpha'],
|
||||
components: {
|
||||
IconChevronDown: IconChevronDown(),
|
||||
ColorSelect: colorSelect,
|
||||
AlphaSelect: alphaSelect,
|
||||
TinyButton: Button,
|
||||
TinyButtonGroup: ButtonGroup,
|
||||
TinyInput: Input
|
||||
},
|
||||
directives: directive({ Clickoutside }),
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api, extendOptions: { language } })
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
let isDragging = false
|
||||
|
||||
export interface DraggableOptions {
|
||||
drag?: (event: MouseEvent | TouchEvent) => void
|
||||
start?: (event: MouseEvent | TouchEvent) => void
|
||||
end?: (event: MouseEvent | TouchEvent) => void
|
||||
}
|
||||
|
||||
export function draggable(element: HTMLElement, options: DraggableOptions) {
|
||||
const moveFn = function (event: MouseEvent | TouchEvent) {
|
||||
options.drag?.(event)
|
||||
}
|
||||
|
||||
const upFn = function (event: MouseEvent | TouchEvent) {
|
||||
document.removeEventListener('mousemove', moveFn)
|
||||
document.removeEventListener('mouseup', upFn)
|
||||
document.removeEventListener('touchmove', moveFn)
|
||||
document.removeEventListener('touchend', upFn)
|
||||
document.onselectstart = null
|
||||
document.ondragstart = null
|
||||
|
||||
isDragging = false
|
||||
|
||||
options.end?.(event)
|
||||
}
|
||||
|
||||
const downFn = function (event: MouseEvent | TouchEvent) {
|
||||
if (isDragging) return
|
||||
event.preventDefault()
|
||||
document.onselectstart = () => false
|
||||
document.ondragstart = () => false
|
||||
document.addEventListener('mousemove', moveFn)
|
||||
document.addEventListener('mouseup', upFn)
|
||||
document.addEventListener('touchmove', moveFn)
|
||||
document.addEventListener('touchend', upFn)
|
||||
|
||||
isDragging = true
|
||||
|
||||
options.start?.(event)
|
||||
}
|
||||
|
||||
element.addEventListener('mousedown', downFn)
|
||||
element.addEventListener('touchstart', downFn)
|
||||
}
|
Loading…
Reference in New Issue