Merge pull request #405 from Budibase/Table-component-and-minor-ui-fixes

WIP - Table component revised plus minor UI changes [requires table sort]
This commit is contained in:
Joe 2020-07-28 12:29:20 +01:00 committed by GitHub
commit af67671273
9 changed files with 619 additions and 38 deletions

View File

@ -0,0 +1,320 @@
<script>
import { onMount, createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import Swatch from "./Swatch.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"
import Portal from "./Portal.svelte"
export let value = "#3ec1d3ff"
export let open = false
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa"
export let style = ""
export let pickerHeight = 0
export let pickerWidth = 0
let colorPicker = null
let adder = null
let h = null
let s = null
let v = null
let a = null
const dispatch = createEventDispatcher()
onMount(() => {
if (!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
getRecentColors()
}
if (colorPicker) {
colorPicker.focus()
}
if (format) {
convertAndSetHSVA()
}
})
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors")
if (colorStore) {
swatches = JSON.parse(colorStore)
}
}
function handleEscape(e) {
if (open && e.key === "Escape") {
open = false
}
}
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
}
if (!swatches.includes(color)) {
swatches = [...swatches, color]
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
}
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)
dispatchValue()
}
function setHue({ color, isDrag }) {
h = color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function setAlpha({ color, isDrag }) {
a = color === "1.00" ? "1" : color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function dispatchValue() {
dispatch("change", value)
}
function changeFormatAndConvert(f) {
format = f
value = convertHsvaToFormat([h, s, v, a], format)
}
function handleColorInput(text) {
let format = getColorFormat(text)
if (format) {
value = text
convertAndSetHSVA()
}
}
function dispatchInputChange() {
if (format) {
dispatchValue()
}
}
function addSwatch() {
if (format) {
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let removedSwatch = swatches.splice(idx, 1)
swatches = swatches
dispatch("removeswatch", removedSwatch)
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
function applySwatch(color) {
if (value !== color) {
format = getColorFormat(color)
if (format) {
value = color
convertAndSetHSVA()
dispatchValue()
}
}
}
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<Portal>
<div
class="colorpicker-container"
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
on:keydown={handleEscape}
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<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={selectedColorStyle} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue} />
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div>
</div>
</Portal>
<style>
.colorpicker-container {
position: absolute;
outline: none;
z-index: 3;
display: flex;
font-size: 11px;
font-weight: 400;
flex-direction: column;
margin: 5px 0px;
height: auto;
width: 220px;
background: #ffffff;
border-radius: 2px;
box-shadow: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1),
0 0 1em 0 rgba(0, 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%;
}
.swatch-panel {
flex: 0 0 15px;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
padding: 0 5px;
max-height: 56px;
}
.adder {
flex: 1;
height: 20px;
display: flex;
transition: flex 0.5s;
justify-content: center;
align-items: center;
background: #f1f3f4;
cursor: pointer;
border: 1px solid #d4d4d4;
border-radius: 8px;
margin-left: 5px;
margin-top: 3px;
font-weight: 500;
}
.shrink {
flex: 0 0 20px;
}
.format-input-panel {
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 3px;
}
</style>

View File

@ -0,0 +1,157 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, beforeUpdate } from "svelte"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
import { getColorFormat } from "./utils.js"
export let value = "#3ec1d3ff"
export let swatches = []
export let disableSwatches = false
export let open = false
export let width = "25px"
export let height = "25px"
let format = "hexa"
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null
let previewHeight = null
let previewWidth = null
let pickerWidth = 0
let pickerHeight = 0
let errorMsg = null
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
}
})
function openColorpicker(event) {
if (colorPreview) {
open = true
}
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left,
} = colorPreview.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
const previewCenter = previewWidth / 2
let y, x
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
y = window.innerHeight - spaceAbove
} else {
positionSide = "top"
y = bottom
}
x = left + previewCenter - pickerWidth / 2
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
}
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</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}
<Colorpicker
style={pickerStyle}
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
bind:open
{swatches}
{disableSwatches} />
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
<div class="color-preview preview-error" style={errorPreviewStyle}>
<span>&times;</span>
</div>
{/if}
</div>
<style>
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
}
.color-preview {
cursor: pointer;
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>

View File

@ -0,0 +1,37 @@
<script>
import { onMount } from "svelte"
export let target = document.body
let targetEl
let portal
let componentInstance
onMount(() => {
if (typeof target === "string") {
targetEl = document.querySelector(target)
// Force exit
if (targetEl === null) {
return () => {}
}
} else if (target instanceof HTMLElement) {
targetEl = target
} else {
throw new TypeError(
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.`
)
}
portal = document.createElement("div")
targetEl.appendChild(portal)
portal.appendChild(componentInstance)
return () => {
targetEl.removeChild(portal)
}
})
</script>
<div bind:this={componentInstance}>
<slot />
</div>

View File

@ -99,9 +99,7 @@
{selectedCategory} /> {selectedCategory} />
{#if displayName} {#if displayName}
<div class="instance-name"> <div class="instance-name">{componentInstance._instanceName}</div>
<strong>{componentInstance._instanceName}</strong>
</div>
{/if} {/if}
<div class="component-props-container"> <div class="component-props-container">
@ -142,13 +140,15 @@
} }
.component-props-container { .component-props-container {
margin-top: 10px; margin-top: 16px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
} }
.instance-name { .instance-name {
margin-top: 10px; margin-top: 20px;
font-size: 12px; font-size: 14px;
font-weight: 500;
color: var(--grey-7);
} }
</style> </style>

View File

@ -52,7 +52,6 @@
} }
.label { .label {
flex: 0 0 50px;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;

View File

@ -85,6 +85,8 @@ export const margin = [
{ label: "20px", value: "20px" }, { label: "20px", value: "20px" },
{ label: "32px", value: "32px" }, { label: "32px", value: "32px" },
{ label: "64px", value: "64px" }, { label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" }, { label: "Auto", value: "auto" },
{ label: "100%", value: "100%" }, { label: "100%", value: "100%" },
], ],
@ -101,6 +103,8 @@ export const margin = [
{ label: "20px", value: "20px" }, { label: "20px", value: "20px" },
{ label: "32px", value: "32px" }, { label: "32px", value: "32px" },
{ label: "64px", value: "64px" }, { label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" }, { label: "Auto", value: "auto" },
{ label: "100%", value: "100%" }, { label: "100%", value: "100%" },
], ],
@ -133,6 +137,8 @@ export const margin = [
{ label: "20px", value: "20px" }, { label: "20px", value: "20px" },
{ label: "32px", value: "32px" }, { label: "32px", value: "32px" },
{ label: "64px", value: "64px" }, { label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" }, { label: "Auto", value: "auto" },
{ label: "100%", value: "100%" }, { label: "100%", value: "100%" },
], ],
@ -149,6 +155,8 @@ export const margin = [
{ label: "20px", value: "20px" }, { label: "20px", value: "20px" },
{ label: "32px", value: "32px" }, { label: "32px", value: "32px" },
{ label: "64px", value: "64px" }, { label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" }, { label: "Auto", value: "auto" },
{ label: "100%", value: "100%" }, { label: "100%", value: "100%" },
], ],

View File

@ -290,7 +290,14 @@ export default {
icon: "ri-archive-drawer-line", icon: "ri-archive-drawer-line",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }], settings: [
{ label: "Model", key: "model", control: ModelSelect },
{ label: "Stripe Color", key: "stripeColor", control: Input },
{ label: "Border Color", key: "borderColor", control: Input },
{ label: "TH Color", key: "backgroundColor", control: Input },
{ label: "TH Font Color", key: "color", control: Input },
{ label: "Table", key: "model", control: ModelSelect },
],
}, },
children: [], children: [],
}, },

View File

@ -191,16 +191,22 @@
"icon": { "icon": {
"description": "A HTML icon tag", "description": "A HTML icon tag",
"props": { "props": {
"icon": "string", "url": "string",
"fontSize": "string", "className": "string",
"color": "string" "description": "string",
"height": "string",
"width": "string"
} }
}, },
"datatable": { "datatable": {
"description": "an HTML table that fetches data from a table or view and displays it.", "description": "an HTML table that fetches data from a table or view and displays it.",
"data": true, "data": true,
"props": { "props": {
"model": "models" "model": "models",
"stripeColor": "string",
"borderColor": "string",
"backgroundColor": "string",
"color": "string"
} }
}, },
"dataform": { "dataform": {

View File

@ -1,13 +1,25 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { cssVars, createClasses } from "./cssVars"
export let _bb export let _bb
export let onLoad export let onLoad
export let model export let model
export let backgroundColor
export let color
export let stripeColor
export let borderColor
let headers = [] let headers = []
let store = _bb.store let store = _bb.store
$: cssVariables = {
backgroundColor,
color,
stripeColor,
borderColor,
}
const shouldDisplayField = name => { const shouldDisplayField = name => {
if (name.startsWith("_")) return false if (name.startsWith("_")) return false
// always 'record' // always 'record'
@ -44,7 +56,7 @@
}) })
</script> </script>
<table class="uk-table"> <table use:cssVars={cssVariables}>
<thead> <thead>
<tr> <tr>
{#each headers as header} {#each headers as header}
@ -67,42 +79,77 @@
<style> <style>
table { table {
border: 1px solid #ccc; width: 100%;
background: #fff;
border-radius: 3px;
border-collapse: collapse; border-collapse: collapse;
overflow: scroll; /* Scrollbar are always visible */ overflow: scroll; /* Scrollbar are always visible */
overflow: auto; /* Scrollbar is displayed as it's needed */ overflow: auto; /* Scrollbar is displayed as it's needed */
} }
thead { /* Zebra striping */
background: #393c44; tr:nth-of-type(odd) {
border: 1px solid #ccc; background: var(--stripeColor);
height: 40px;
text-align: left;
margin-right: 60px;
} }
thead th { th {
color: #ffffff; background-color: var(--backgroundColor);
color: var(--color);
font-weight: bold;
text-transform: capitalize; text-transform: capitalize;
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
justify-content: left;
padding: 16px 20px 16px 8px;
margin-right: 20px;
} }
tbody tr { td,
border-bottom: 1px solid #ccc; th {
transition: 0.3s background-color; padding: 16px;
color: #393c44; border: 1px solid var(--borderColor);
font-size: 14px; text-align: left;
height: 40px;
} }
tbody tr:hover { @media only screen and (max-width: 760px),
background: var(--grey-1); (min-device-width: 768px) and (max-device-width: 1024px) {
table {
width: 100%;
}
/* Force table to not be like tables anymore */
table,
thead,
tbody,
th,
td,
tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
tr {
border: 1px solid var(--borderColor);
}
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 10%;
}
td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
/* Label the data */
content: attr(data-column);
}
} }
</style> </style>