mirror of https://github.com/Budibase/budibase.git
25 changed files with 1052 additions and 25 deletions
@ -0,0 +1,29 @@ |
|||||
|
<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; |
||||
|
justify-content: center; |
||||
|
width: 170px; |
||||
|
height: 30px; |
||||
|
align-self: center; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<div class="flatbutton-group"> |
||||
|
{#each colorFormats as text} |
||||
|
<FlatButton |
||||
|
selected={format === text} |
||||
|
{text} |
||||
|
on:click={() => onclick(text)} /> |
||||
|
{/each} |
||||
|
</div> |
||||
@ -0,0 +1,23 @@ |
|||||
|
<script> |
||||
|
import {buildStyle} from "./helpers.js" |
||||
|
|
||||
|
export let backgroundSize = "10px" |
||||
|
export let borderRadius = "" |
||||
|
export let height = "" |
||||
|
export let width = "" |
||||
|
|
||||
|
$: style = buildStyle({backgroundSize, borderRadius, height, width}) |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
background-image: url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="white" d="M1,0H2V1H1V0ZM0,1H1V2H0V1Z"/><path fill="gray" d="M0,0H1V1H0V0ZM1,1H2V2H1V1Z"/></svg>'); |
||||
|
height: fit-content; |
||||
|
width: fit-content; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<div {style}> |
||||
|
<slot /> |
||||
|
</div> |
||||
@ -0,0 +1,160 @@ |
|||||
|
<script> |
||||
|
import { onMount, createEventDispatcher } from "svelte"; |
||||
|
import CheckedBackground from "./CheckedBackground.svelte" |
||||
|
import {buildStyle} from "./helpers.js" |
||||
|
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; |
||||
|
value = convertHsvaToFormat([h, s, v, a], format); |
||||
|
} |
||||
|
|
||||
|
function handleColorInput(text) { |
||||
|
let f = getColorFormat(text) |
||||
|
if(f) { |
||||
|
format = f; |
||||
|
value = text |
||||
|
convertAndSetHSVA() |
||||
|
dispatch("change", value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$: border = s < 10 ? "1px dashed #dedada" : "" |
||||
|
$: style = buildStyle({background: value, border}) |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.colorpicker-container { |
||||
|
display: flex; |
||||
|
font-size: 11px; |
||||
|
font-weight: 400; |
||||
|
flex-direction: column; |
||||
|
height: 265px; |
||||
|
width: 220px; |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
.palette-panel { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.control-panel { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 8px; |
||||
|
background: white; |
||||
|
border: 1px solid #d2d2d2; |
||||
|
color: #777373; |
||||
|
} |
||||
|
|
||||
|
.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%; |
||||
|
} |
||||
|
|
||||
|
.format-input-panel { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<div class="colorpicker-container"> |
||||
|
|
||||
|
<div class="palette-panel"> |
||||
|
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="control-panel"> |
||||
|
<div class="alpha-hue-panel"> |
||||
|
<div> |
||||
|
<CheckedBackground borderRadius="50%" backgroundSize="8px"> |
||||
|
<div class="selected-color" {style} /> |
||||
|
</CheckedBackground> |
||||
|
</div> |
||||
|
<div> |
||||
|
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} /> |
||||
|
|
||||
|
<CheckedBackground borderRadius="10px" backgroundSize="7px"> |
||||
|
<Slider |
||||
|
type="alpha" |
||||
|
value={a} |
||||
|
on:change={alpha => setAlpha(alpha.detail)} /> |
||||
|
</CheckedBackground> |
||||
|
|
||||
|
</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,146 @@ |
|||||
|
<script> |
||||
|
import Colorpicker from "./Colorpicker.svelte" |
||||
|
import CheckedBackground from "./CheckedBackground.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) : 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} |
||||
|
<CheckedBackground borderRadius="3px" backgroundSize="8px"> |
||||
|
<div bind:this={colorPreview} bind:clientHeight={previewHeight} bind:clientWidth={previewWidth} class="color-preview" style={previewStyle} on:click={openColorpicker} /> |
||||
|
</CheckedBackground> |
||||
|
|
||||
|
{#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 { |
||||
|
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,29 @@ |
|||||
|
<script> |
||||
|
export let text = ""; |
||||
|
export let selected = false; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.flatbutton { |
||||
|
cursor: pointer; |
||||
|
border: 1px solid #d4d4d4; |
||||
|
border-radius: 8px; |
||||
|
text-transform: uppercase; |
||||
|
margin: 5px; |
||||
|
transition: all 0.3s; |
||||
|
font-size: 10px; |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: #f1f3f4; |
||||
|
} |
||||
|
|
||||
|
.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: 175px; |
||||
|
font-size: 13px; |
||||
|
background: #f1f3f4; |
||||
|
border-radius: 8px; |
||||
|
height: 20px; |
||||
|
outline-color: #003cb0; |
||||
|
color: inherit; |
||||
|
text-align: center; |
||||
|
border: 1px solid #dadada; |
||||
|
font-weight: 550; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<div> |
||||
|
<input on:input type="text" {value} maxlength="25" /> |
||||
|
</div> |
||||
@ -0,0 +1,63 @@ |
|||||
|
<script> |
||||
|
import { onMount, createEventDispatcher } from "svelte"; |
||||
|
import CheckedBackground from "./CheckedBackground.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: 140px; |
||||
|
cursor: crosshair; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.picker { |
||||
|
position: absolute; |
||||
|
width: 10px; |
||||
|
height: 10px; |
||||
|
background: transparent; |
||||
|
border: 2px solid white; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<CheckedBackground width="100%"> |
||||
|
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}> |
||||
|
<div class="picker" style={pickerStyle} /> |
||||
|
</div> |
||||
|
</CheckedBackground> |
||||
@ -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: 150px; |
||||
|
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() |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,11 @@ |
|||||
|
FROM node:12-alpine |
||||
|
|
||||
|
WORKDIR /app |
||||
|
|
||||
|
# copy files and install dependencies |
||||
|
COPY . ./ |
||||
|
RUN yarn |
||||
|
|
||||
|
EXPOSE 4001 |
||||
|
|
||||
|
CMD ["yarn", "run:docker"] |
||||
@ -0,0 +1,13 @@ |
|||||
|
const { resolve } = require("path") |
||||
|
|
||||
|
async function runServer() { |
||||
|
const budibaseDir = "~/.budibase" |
||||
|
|
||||
|
process.env.BUDIBASE_DIR = resolve(budibaseDir) |
||||
|
|
||||
|
const server = await require("./app")() |
||||
|
server.on("close", () => console.log("Server Closed")) |
||||
|
console.log(`Budibase running on ${JSON.stringify(server.address())}`) |
||||
|
} |
||||
|
|
||||
|
runServer() |
||||
Loading…
Reference in new issue