Merge pull request #364 from Budibase/property-panel/colorpicker
Integrating Colorpicker with Builder
This commit is contained in:
commit
b5cb6a2aea
17 changed files with 956 additions and 10 deletions
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import FlatButton from "./FlatButton.svelte";
|
||||||
|
|
||||||
|
export let format = "hex";
|
||||||
|
export let onclick = format => {};
|
||||||
|
|
||||||
|
let colorFormats = ["hex", "rgb", "hsl"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flatbutton-group {
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="flatbutton-group">
|
||||||
|
{#each colorFormats as text}
|
||||||
|
<FlatButton
|
||||||
|
selected={format === text}
|
||||||
|
{text}
|
||||||
|
on:click={() => onclick(text)} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
|
@ -0,0 +1,145 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getColorFormat,
|
||||||
|
convertToHSVA,
|
||||||
|
convertHsvaToFormat
|
||||||
|
} from "./utils.js";
|
||||||
|
import Slider from "./Slider.svelte";
|
||||||
|
import Palette from "./Palette.svelte";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import Input from "./Input.svelte";
|
||||||
|
|
||||||
|
export let value = "#3ec1d3ff";
|
||||||
|
export let format = "hexa";
|
||||||
|
|
||||||
|
let h = null;
|
||||||
|
let s = null;
|
||||||
|
let v = null;
|
||||||
|
let a = null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (format) {
|
||||||
|
convertAndSetHSVA()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function convertAndSetHSVA() {
|
||||||
|
let hsva = convertToHSVA(value, format);
|
||||||
|
setHSVA(hsva);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHSVA([hue, sat, val, alpha]) {
|
||||||
|
h = hue;
|
||||||
|
s = sat;
|
||||||
|
v = val;
|
||||||
|
a = alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
//fired by choosing a color from the palette
|
||||||
|
function setSaturationAndValue({ detail }) {
|
||||||
|
s = detail.s;
|
||||||
|
v = detail.v;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHue(hue) {
|
||||||
|
h = hue;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAlpha(alpha) {
|
||||||
|
a = alpha === "1.00" ? "1" :alpha;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeFormatAndConvert(f) {
|
||||||
|
format = f;
|
||||||
|
console.log(f)
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColorInput(text) {
|
||||||
|
let f = getColorFormat(text)
|
||||||
|
if(f) {
|
||||||
|
format = f;
|
||||||
|
value = text
|
||||||
|
convertAndSetHSVA()
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.colorpicker-container {
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 300px;
|
||||||
|
width: 250px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0.15em 1.5em 0 rgba(0,0,0,.1), 0 0 1em 0 rgba(0,0,0,.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d2d2d2
|
||||||
|
}
|
||||||
|
|
||||||
|
.alpha-hue-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 25px 1fr;
|
||||||
|
grid-gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-color {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #dedada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-input-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="colorpicker-container">
|
||||||
|
|
||||||
|
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
|
||||||
|
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="alpha-hue-panel">
|
||||||
|
<div>
|
||||||
|
<div class="selected-color" style={`background: ${value}`} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} />
|
||||||
|
<Slider
|
||||||
|
type="alpha"
|
||||||
|
value={a}
|
||||||
|
on:change={alpha => setAlpha(alpha.detail)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="format-input-panel">
|
||||||
|
<ButtonGroup {format} onclick={changeFormatAndConvert} />
|
||||||
|
<Input {value} on:input={event => handleColorInput(event.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,143 @@
|
||||||
|
<script>
|
||||||
|
import Colorpicker from "./Colorpicker.svelte"
|
||||||
|
import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte"
|
||||||
|
import {buildStyle} from "./helpers.js"
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import {getColorFormat} from "./utils.js"
|
||||||
|
|
||||||
|
export let value = "#3ec1d3ff"
|
||||||
|
export let open = false;
|
||||||
|
export let width = "25px"
|
||||||
|
export let height = "25px"
|
||||||
|
|
||||||
|
let format = "hexa";
|
||||||
|
let dimensions = {top: 0, left: 0}
|
||||||
|
let colorPreview = null
|
||||||
|
|
||||||
|
let previewHeight = null
|
||||||
|
let previewWidth = null
|
||||||
|
let pickerWidth = 250
|
||||||
|
let pickerHeight = 300
|
||||||
|
|
||||||
|
let anchorEl = null
|
||||||
|
let parentNodes = [];
|
||||||
|
let errorMsg = null
|
||||||
|
|
||||||
|
$: previewStyle = buildStyle({width, height, background: value})
|
||||||
|
$: errorPreviewStyle = buildStyle({width, height})
|
||||||
|
$: pickerStyle = buildStyle({top: `${dimensions.top}px`, left: `${dimensions.left}px`})
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
format = getColorFormat(value)
|
||||||
|
if(!format) {
|
||||||
|
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
|
||||||
|
console.error(errorMsg)
|
||||||
|
}else{
|
||||||
|
errorMsg = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if(colorPreview && colorPreview.offsetParent && !anchorEl) {
|
||||||
|
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
|
||||||
|
anchorEl = colorPreview.offsetParent
|
||||||
|
let curEl = colorPreview
|
||||||
|
let els = []
|
||||||
|
//Travel up dom tree from preview element to find parent elements that scroll
|
||||||
|
while(!anchorEl.isSameNode(curEl)) {
|
||||||
|
curEl = curEl.parentNode
|
||||||
|
let elOverflow = window.getComputedStyle(curEl).getPropertyValue("overflow")
|
||||||
|
if(/scroll|auto/.test(elOverflow)) {
|
||||||
|
els.push(curEl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentNodes = els
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function openColorpicker(event) {
|
||||||
|
if(colorPreview) {
|
||||||
|
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
|
||||||
|
const {innerHeight, innerWidth} = window
|
||||||
|
|
||||||
|
const {offsetLeft, offsetTop} = colorPreview
|
||||||
|
//get the scrollTop value for all scrollable parent elements
|
||||||
|
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
|
||||||
|
|
||||||
|
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
|
||||||
|
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
|
||||||
|
|
||||||
|
//TOO: Testing and Scroll Awareness for x Scroll
|
||||||
|
const spaceRight = (innerWidth - spaceLeft) + previewWidth
|
||||||
|
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) + pickerWidth : offsetLeft - pickerWidth
|
||||||
|
|
||||||
|
dimensions = {top, left}
|
||||||
|
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onColorChange(color) {
|
||||||
|
value = color.detail;
|
||||||
|
dispatch("change", color.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="color-preview-container">
|
||||||
|
{#if !errorMsg}
|
||||||
|
<div bind:this={colorPreview} bind:clientHeight={previewHeight} bind:clientWidth={previewWidth} class="color-preview" style={previewStyle} on:click={openColorpicker}></div>
|
||||||
|
{#if open}
|
||||||
|
<div class="picker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth} style={pickerStyle}>
|
||||||
|
<Colorpicker on:change={onColorChange} {format} {value} />
|
||||||
|
</div>
|
||||||
|
<div on:click|self={() => open = false} class="overlay"></div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="color-preview preview-error" style={errorPreviewStyle}>
|
||||||
|
<span>×</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color-preview-container{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #dedada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error {
|
||||||
|
background: #cccccc;
|
||||||
|
color: #808080;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay{
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
export let text = "";
|
||||||
|
export let selected = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flatbutton {
|
||||||
|
padding: 3px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
background-color: #dadada;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #003cb0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="flatbutton" class:selected on:click>{text}</div>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script>
|
||||||
|
export let value = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 185px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 27px;
|
||||||
|
outline-color: #003cb0;
|
||||||
|
color: #565252;
|
||||||
|
text-align: center;
|
||||||
|
/* border: none; */
|
||||||
|
border: 1px solid #dadada;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input on:input type="text" {value} maxlength="25" />
|
||||||
|
</div>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let h = 0;
|
||||||
|
export let s = 0;
|
||||||
|
export let v = 0;
|
||||||
|
export let a = 1;
|
||||||
|
|
||||||
|
let palette;
|
||||||
|
|
||||||
|
let paletteHeight, paletteWidth = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
const { left, top } = palette.getBoundingClientRect();
|
||||||
|
let clickX = (event.clientX - left)
|
||||||
|
let clickY = (event.clientY - top)
|
||||||
|
if((clickX > 0 && clickY > 0) && (clickX < paletteWidth && clickY < paletteHeight)) {
|
||||||
|
let s = (clickX / paletteWidth) * 100
|
||||||
|
let v = 100 - ((clickY / paletteHeight) * 100)
|
||||||
|
dispatch("change", {s, v})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$: pickerX = (s * paletteWidth) / 100;
|
||||||
|
$: pickerY = paletteHeight * ((100 - v) / 100)
|
||||||
|
|
||||||
|
$: paletteGradient = `linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
|
||||||
|
linear-gradient(to left, hsla(${h}, 100%, 50%, ${a}), rgba(255, 255, 255, ${a}))
|
||||||
|
`;
|
||||||
|
$: style = `background: ${paletteGradient};`;
|
||||||
|
|
||||||
|
$: pickerStyle = `transform: translate(${pickerX - 8}px, ${pickerY - 8}px);`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.palette {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 175px;
|
||||||
|
cursor: crosshair;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}>
|
||||||
|
<div class="picker" style={pickerStyle} />
|
||||||
|
</div>
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
import dragable from "./drag.js";
|
||||||
|
|
||||||
|
export let value = 1;
|
||||||
|
export let type = "hue";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let slider;
|
||||||
|
let sliderWidth = 0;
|
||||||
|
|
||||||
|
function handleClick(mouseX) {
|
||||||
|
const { left, width } = slider.getBoundingClientRect();
|
||||||
|
let clickPosition = mouseX - left;
|
||||||
|
|
||||||
|
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
|
||||||
|
|
||||||
|
if (percentageClick >= 0 && percentageClick <= 1) {
|
||||||
|
let value =
|
||||||
|
type === "hue"
|
||||||
|
? 360 * percentageClick
|
||||||
|
: percentageClick;
|
||||||
|
dispatch("change", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: thumbPosition =
|
||||||
|
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value;
|
||||||
|
|
||||||
|
$: style = `transform: translateX(${thumbPosition - 6}px);`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color-format-slider {
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
height: 8px;
|
||||||
|
width: 185px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
border: 1px solid #e8e8ef;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hue {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
hsl(0, 100%, 50%),
|
||||||
|
hsl(60, 100%, 50%),
|
||||||
|
hsl(120, 100%, 50%),
|
||||||
|
hsl(180, 100%, 50%),
|
||||||
|
hsl(240, 100%, 50%),
|
||||||
|
hsl(300, 100%, 50%),
|
||||||
|
hsl(360, 100%, 50%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alpha {
|
||||||
|
background: linear-gradient(to right, transparent, rgb(0 0 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-thumb {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -3px;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border: 1px solid #777676;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
cursor:grab;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={slider}
|
||||||
|
bind:clientWidth={sliderWidth}
|
||||||
|
on:click={event => handleClick(event.clientX)}
|
||||||
|
class="color-format-slider"
|
||||||
|
class:hue={type === 'hue'}
|
||||||
|
class:alpha={type === 'alpha'}>
|
||||||
|
<div
|
||||||
|
use:dragable
|
||||||
|
on:drag={e => handleClick(e.detail)}
|
||||||
|
class="slider-thumb"
|
||||||
|
{style} />
|
||||||
|
</div>
|
|
@ -0,0 +1,22 @@
|
||||||
|
export default function(node) {
|
||||||
|
function handleMouseDown() {
|
||||||
|
window.addEventListener("mousemove", handleMouseMove)
|
||||||
|
window.addEventListener("mouseup", handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event) {
|
||||||
|
let mouseX = event.clientX
|
||||||
|
node.dispatchEvent(
|
||||||
|
new CustomEvent("drag", {
|
||||||
|
detail: mouseX,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
window.removeEventListener("mousedown", handleMouseDown)
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("mousedown", handleMouseDown)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const buildStyle = styles => {
|
||||||
|
let str = ""
|
||||||
|
for (let s in styles) {
|
||||||
|
if (styles[s]) {
|
||||||
|
let key = convertCamel(s)
|
||||||
|
str += `${key}: ${styles[s]}; `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertCamel = str => {
|
||||||
|
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Colorpreview from "./Colorpreview.svelte"
|
||||||
|
export default Colorpreview
|
|
@ -0,0 +1,279 @@
|
||||||
|
export const isValidHex = str =>
|
||||||
|
/^#(?:[A-F0-9]{3}$|[A-F0-9]{4}$|[A-F0-9]{6}$|[A-F0-9]{8})$/gi.test(str)
|
||||||
|
|
||||||
|
const getHexaValues = hexString => {
|
||||||
|
if (hexString.length <= 5) {
|
||||||
|
let hexArr = hexString.match(/[A-F0-9]/gi)
|
||||||
|
let t = hexArr.map(c => (c += c))
|
||||||
|
return t
|
||||||
|
} else {
|
||||||
|
return hexString.match(/[A-F0-9]{2}/gi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidRgb = str => {
|
||||||
|
const hasValidStructure = /^(?:rgba\(|rgb\()(?:[0-9,\s]|\.(?=\d))*\)$/gi.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
if (hasValidStructure) {
|
||||||
|
return testRgbaValues(str.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNonNumericChars = /[a-z()\s]/gi
|
||||||
|
|
||||||
|
export const getNumericValues = str =>
|
||||||
|
str
|
||||||
|
.replace(findNonNumericChars, "")
|
||||||
|
.split(",")
|
||||||
|
.map(v => (v !== "" ? v : undefined))
|
||||||
|
|
||||||
|
export const testRgbaValues = str => {
|
||||||
|
const rgba = getNumericValues(str)
|
||||||
|
const [r, g, b, a] = rgba
|
||||||
|
|
||||||
|
let isValidLengthRange =
|
||||||
|
(str.startsWith("rgb(") && rgba.length === 3) ||
|
||||||
|
(str.startsWith("rgba(") && rgba.length === 4)
|
||||||
|
let isValidColorRange = [r, g, b].every(v => v >= 0 && v <= 255)
|
||||||
|
let isValidAlphaRange = str.startsWith("rgba(")
|
||||||
|
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||||
|
: true
|
||||||
|
|
||||||
|
return isValidLengthRange && isValidColorRange && isValidAlphaRange
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidHsl = str => {
|
||||||
|
const hasValidStructure = /^(?:hsl\(|hsla\()(?:[0-9,%\s]|\.(?=\d))*\)$/gi.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
if (hasValidStructure) {
|
||||||
|
return testHslaValues(str.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testHslaValues = str => {
|
||||||
|
const hsla = getNumericValues(str)
|
||||||
|
const [h, s, l, a] = hsla
|
||||||
|
const isUndefined = [h, s, l].some(v => v === undefined)
|
||||||
|
|
||||||
|
if (isUndefined) return false
|
||||||
|
|
||||||
|
let isValidLengthRange =
|
||||||
|
(str.startsWith("hsl(") && hsla.length === 3) ||
|
||||||
|
(str.startsWith("hsla(") && hsla.length === 4)
|
||||||
|
let isValidHue = h >= 0 && h <= 360
|
||||||
|
let isValidSatLum = [s, l].every(
|
||||||
|
v => v.endsWith("%") && parseInt(v) >= 0 && parseInt(v) <= 100
|
||||||
|
)
|
||||||
|
let isValidAlphaRange = str.startsWith("hsla(")
|
||||||
|
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||||
|
: true
|
||||||
|
|
||||||
|
return isValidLengthRange && isValidHue && isValidSatLum && isValidAlphaRange
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getColorFormat = color => {
|
||||||
|
if (typeof color === "string") {
|
||||||
|
if (isValidHex(color)) {
|
||||||
|
return "hex"
|
||||||
|
} else if (isValidRgb(color)) {
|
||||||
|
return "rgb"
|
||||||
|
} else if (isValidHsl(color)) {
|
||||||
|
return "hsl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToHSVA = (value, format) => {
|
||||||
|
switch (format) {
|
||||||
|
case "hex":
|
||||||
|
return getAndConvertHexa(value)
|
||||||
|
case "rgb":
|
||||||
|
return getAndConvertRgba(value)
|
||||||
|
case "hsl":
|
||||||
|
return getAndConvertHsla(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertHsvaToFormat = (hsva, format) => {
|
||||||
|
switch (format) {
|
||||||
|
case "hex":
|
||||||
|
return hsvaToHexa(hsva, true)
|
||||||
|
case "rgb":
|
||||||
|
return hsvaToRgba(hsva, true)
|
||||||
|
case "hsl":
|
||||||
|
return hsvaToHsla(hsva)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertHexa = color => {
|
||||||
|
let [rHex, gHex, bHex, aHex] = getHexaValues(color)
|
||||||
|
return hexaToHSVA([rHex, gHex, bHex], aHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertRgba = color => {
|
||||||
|
let rgba = getNumericValues(color)
|
||||||
|
return rgbaToHSVA(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertHsla = color => {
|
||||||
|
let hsla = getNumericValues(color)
|
||||||
|
return hslaToHSVA(hsla)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hexaToHSVA = (hex, alpha = "FF") => {
|
||||||
|
const rgba = hex
|
||||||
|
.map(v => parseInt(v, 16))
|
||||||
|
.concat(Number((parseInt(alpha, 16) / 255).toFixed(2)))
|
||||||
|
return rgbaToHSVA(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rgbaToHSVA = rgba => {
|
||||||
|
const [r, g, b, a = 1] = rgba
|
||||||
|
let hsv = _rgbToHSV([r, g, b])
|
||||||
|
return [...hsv, a].map(x => parseFloat(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hslaToHSVA = ([h, s, l, a = 1]) => {
|
||||||
|
let sat = s.replace(/%/, "")
|
||||||
|
let lum = l.replace(/%/, "")
|
||||||
|
let hsv = _hslToHSV([h, sat, lum])
|
||||||
|
return [...hsv, a].map(x => parseFloat(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToHexa = (hsva, asString = false) => {
|
||||||
|
const [r, g, b, a] = hsvaToRgba(hsva)
|
||||||
|
|
||||||
|
const hexa = [r, g, b]
|
||||||
|
.map(v => {
|
||||||
|
let hex = Math.round(v).toString(16)
|
||||||
|
return hex.length === 1 ? `0${hex}` : hex
|
||||||
|
})
|
||||||
|
.concat(Math.round(a * 255).toString(16))
|
||||||
|
return asString ? `#${hexa.join("")}` : hexa
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToRgba = ([h, s, v, a = 1], asString = false) => {
|
||||||
|
let rgb = _hsvToRgb([h, s, v]).map(x => Math.round(x))
|
||||||
|
let rgba = [...rgb, a < 1 ? _fixNum(a, 2) : a]
|
||||||
|
return asString ? `rgba(${rgba.join(",")})` : rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToHsla = ([h, s, v, a = 1]) => {
|
||||||
|
let [hue, sat, lum] = _hsvToHSL([h, s, v])
|
||||||
|
let hsla = [hue, sat + "%", lum + "%", a < 1 ? _fixNum(a, 2) : a]
|
||||||
|
return `hsla(${hsla.join(",")})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _hslToHSV = hsl => {
|
||||||
|
const h = hsl[0]
|
||||||
|
let s = hsl[1] / 100
|
||||||
|
let l = hsl[2] / 100
|
||||||
|
let smin = s
|
||||||
|
const lmin = Math.max(l, 0.01)
|
||||||
|
|
||||||
|
l *= 2
|
||||||
|
s *= l <= 1 ? l : 2 - l
|
||||||
|
smin *= lmin <= 1 ? lmin : 2 - lmin
|
||||||
|
const v = (l + s) / 2
|
||||||
|
const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s)
|
||||||
|
|
||||||
|
return [h, sv * 100, v * 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _rgbToHSV = rgb => {
|
||||||
|
let rdif
|
||||||
|
let gdif
|
||||||
|
let bdif
|
||||||
|
let h
|
||||||
|
let s
|
||||||
|
|
||||||
|
const r = rgb[0] / 255
|
||||||
|
const g = rgb[1] / 255
|
||||||
|
const b = rgb[2] / 255
|
||||||
|
const v = Math.max(r, g, b)
|
||||||
|
const diff = v - Math.min(r, g, b)
|
||||||
|
const diffc = function(c) {
|
||||||
|
return (v - c) / 6 / diff + 1 / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff === 0) {
|
||||||
|
h = 0
|
||||||
|
s = 0
|
||||||
|
} else {
|
||||||
|
s = diff / v
|
||||||
|
rdif = diffc(r)
|
||||||
|
gdif = diffc(g)
|
||||||
|
bdif = diffc(b)
|
||||||
|
|
||||||
|
if (r === v) {
|
||||||
|
h = bdif - gdif
|
||||||
|
} else if (g === v) {
|
||||||
|
h = 1 / 3 + rdif - bdif
|
||||||
|
} else if (b === v) {
|
||||||
|
h = 2 / 3 + gdif - rdif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h < 0) {
|
||||||
|
h += 1
|
||||||
|
} else if (h > 1) {
|
||||||
|
h -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hsvResult = [h * 360, s * 100, v * 100].map(v => Math.round(v))
|
||||||
|
return hsvResult
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _hsvToRgb = hsv => {
|
||||||
|
const h = hsv[0] / 60
|
||||||
|
const s = hsv[1] / 100
|
||||||
|
let v = hsv[2] / 100
|
||||||
|
const hi = Math.floor(h) % 6
|
||||||
|
|
||||||
|
const f = h - Math.floor(h)
|
||||||
|
const p = 255 * v * (1 - s)
|
||||||
|
const q = 255 * v * (1 - s * f)
|
||||||
|
const t = 255 * v * (1 - s * (1 - f))
|
||||||
|
v *= 255
|
||||||
|
|
||||||
|
switch (hi) {
|
||||||
|
case 0:
|
||||||
|
return [v, t, p]
|
||||||
|
case 1:
|
||||||
|
return [q, v, p]
|
||||||
|
case 2:
|
||||||
|
return [p, v, t]
|
||||||
|
case 3:
|
||||||
|
return [p, q, v]
|
||||||
|
case 4:
|
||||||
|
return [t, p, v]
|
||||||
|
case 5:
|
||||||
|
return [v, p, q]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _hsvToHSL = hsv => {
|
||||||
|
const h = hsv[0]
|
||||||
|
const s = hsv[1] / 100
|
||||||
|
const v = hsv[2] / 100
|
||||||
|
const vmin = Math.max(v, 0.01)
|
||||||
|
let sl
|
||||||
|
let l
|
||||||
|
|
||||||
|
l = (2 - s) * v
|
||||||
|
const lmin = (2 - s) * vmin
|
||||||
|
sl = s * vmin
|
||||||
|
sl /= lmin <= 1 ? lmin : 2 - lmin
|
||||||
|
sl = sl || 0
|
||||||
|
l /= 2
|
||||||
|
|
||||||
|
return [_fixNum(h, 0), _fixNum(sl * 100, 0), _fixNum(l * 100, 0)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _fixNum = (value, decimalPlaces) =>
|
||||||
|
Number(parseFloat(value).toFixed(decimalPlaces))
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { getColorFormat, convertToHSVA, convertHsvaToFormat } from "./utils"
|
||||||
|
|
||||||
|
describe("convertToHSVA - convert to hsva from format", () => {
|
||||||
|
test("convert from hexa", () => {
|
||||||
|
expect(convertToHSVA("#f222d382", "hex")).toEqual([309, 86, 95, 0.51])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from hex", () => {
|
||||||
|
expect(convertToHSVA("#f222d3", "hex")).toEqual([309, 86, 95, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from rgba", () => {
|
||||||
|
expect(convertToHSVA("rgba(242, 34, 211, 1)", "rgb")).toEqual([
|
||||||
|
309,
|
||||||
|
86,
|
||||||
|
95,
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from rgb", () => {
|
||||||
|
expect(convertToHSVA("rgb(150, 80, 255)", "rgb")).toEqual([264, 69, 100, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from from hsl", () => {
|
||||||
|
expect(convertToHSVA("hsl(264, 100%, 65.7%)", "hsl")).toEqual([
|
||||||
|
264,
|
||||||
|
68.6,
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from from hsla", () => {
|
||||||
|
expect(convertToHSVA("hsla(264, 100%, 65.7%, 0.51)", "hsl")).toEqual([
|
||||||
|
264,
|
||||||
|
68.6,
|
||||||
|
100,
|
||||||
|
0.51,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("convertHsvaToFormat - convert from hsva to format", () => {
|
||||||
|
test("Convert to hexa", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 0.5], "hex")).toBe("#9650ff80")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Convert to rgba", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 0.75], "rgb")).toBe(
|
||||||
|
"rgba(150,80,255,0.75)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Convert to hsla", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 1], "hsl")).toBe(
|
||||||
|
"hsla(264,100%,66%,1)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Get Color Format", () => {
|
||||||
|
test("Testing valid hex string", () => {
|
||||||
|
expect(getColorFormat("#FFF")).toBe("hex")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid hex string", () => {
|
||||||
|
expect(getColorFormat("#FFZ")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing valid hex with alpha", () => {
|
||||||
|
expect(getColorFormat("#FF00BB80")).toBe("hex")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Test valid rgb value", () => {
|
||||||
|
expect(getColorFormat("RGB(255, 20, 50)")).toBe("rgb")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid rgb value", () => {
|
||||||
|
expect(getColorFormat("rgb(255, 0)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing rgb value with alpha", () => {
|
||||||
|
expect(getColorFormat("rgba(255, 0, 50, 0.5)")).toBe("rgb")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing rgb value with incorrectly provided alpha", () => {
|
||||||
|
expect(getColorFormat("rgb(255, 0, 50, 0.5)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid hsl value", () => {
|
||||||
|
expect(getColorFormat("hsla(255, 0)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing hsla value with alpha", () => {
|
||||||
|
expect(getColorFormat("hsla(150, 60%, 50%, 0.5)")).toBe("hsl")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing hsl value with incorrectly provided alpha", () => {
|
||||||
|
expect(getColorFormat("hsl(150, 0, 50, 0.5)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing out of bounds hsl", () => {
|
||||||
|
expect(getColorFormat("hsl(375, 0, 50)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -103,8 +103,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -122,6 +120,5 @@
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="positioned-wrapper">
|
||||||
<div class="design-view-property-groups">
|
<div class="design-view-property-groups">
|
||||||
{#if propertyGroupNames.length > 0}
|
{#if propertyGroupNames.length > 0}
|
||||||
{#each propertyGroupNames as groupName}
|
{#each propertyGroupNames as groupName}
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.design-view-container {
|
.design-view-container {
|
||||||
|
@ -62,10 +64,15 @@
|
||||||
flex: 0 0 50px;
|
flex: 0 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.positioned-wrapper{
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.design-view-property-groups {
|
.design-view-property-groups {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
if (v.target) {
|
if (v.target) {
|
||||||
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
|
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||||
onChange(key, val)
|
onChange(key, val)
|
||||||
|
}else if(v.detail) {
|
||||||
|
onChange(key, v.detail)
|
||||||
} else {
|
} else {
|
||||||
onChange(key, v)
|
onChange(key, v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Input from "../common/Input.svelte"
|
||||||
import OptionSelect from "./OptionSelect.svelte"
|
import OptionSelect from "./OptionSelect.svelte"
|
||||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||||
// import Colorpicker from "../common/Colorpicker.svelte"
|
import Colorpicker from "./Colorpicker"
|
||||||
/*
|
/*
|
||||||
TODO: Allow for default values for all properties
|
TODO: Allow for default values for all properties
|
||||||
*/
|
*/
|
||||||
|
@ -256,8 +256,8 @@ export const typography = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "color",
|
key: "color",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
placeholder: "hex",
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "align",
|
label: "align",
|
||||||
|
@ -305,7 +305,8 @@ export const background = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "background",
|
key: "background",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Image",
|
label: "Image",
|
||||||
|
@ -347,7 +348,8 @@ export const border = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "border-color",
|
key: "border-color",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Style",
|
label: "Style",
|
||||||
|
|
|
@ -103,7 +103,6 @@
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
height: calc(100vh - 69px);
|
height: calc(100vh - 69px);
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group-header > div:nth-child(1) {
|
.nav-group-header > div:nth-child(1) {
|
||||||
|
|
Loading…
Reference in a new issue