This commit is contained in:
Gerard Burns 2024-04-01 12:31:16 +01:00
parent ab40e3babd
commit 7eed50707e
8 changed files with 474 additions and 116 deletions

View File

@ -9,8 +9,10 @@
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionsIcon = () => null
export let getOptionsIconToolip = () => null
export let getOptionIcon = () => null
export let getOptionIconTooltip = () => null
export let getOptionTooltip = () => null
export let isOptionEnabled = () => true
export let readonly = false
export let autocomplete = false
export let sort = false
@ -19,6 +21,7 @@
export let customPopoverHeight
export let open = false
export let loading
export let align
const dispatch = createEventDispatcher()
@ -82,8 +85,10 @@
<Picker
on:loadMore
{getOptionsIcon}
{getOptionsIconTooltip}
{isOptionEnabled}
{getOptionIcon}
{getOptionIconTooltip}
{getOptionTooltip}
{id}
{disabled}
{readonly}
@ -101,4 +106,5 @@
{autoWidth}
{customPopoverHeight}
{loading}
{align}
/>

View File

@ -11,6 +11,13 @@
import Tags from "../../Tags/Tags.svelte"
import Tag from "../../Tags/Tag.svelte"
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
import {
default as AbsTooltip,
TooltipPosition,
TooltipType,
} from "../../Tooltip/AbsTooltip.svelte"
import ContextTooltip from "../../Tooltip/Context.svelte"
export let id = null
export let disabled = false
@ -27,6 +34,7 @@
export let getOptionValue = option => option
export let getOptionIcon = () => null
export let getOptionIconTooltip = () => null
export let getOptionTooltip = () => null
export let useOptionIconImage = false
export let getOptionColour = () => null
export let getOptionSubtitle = () => null
@ -48,6 +56,11 @@
let button
let component
let contextTooltipId = 0;
let contextTooltipAnchor = null
let contextTooltipOption = null
let contextTooltipVisible = false
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
$: filteredOptions = getFilteredOptions(
sortedOptions,
@ -103,6 +116,29 @@
onDestroy(() => {
component?.removeEventListener("scroll", null)
})
const handleMouseenter = (e, option) => {
contextTooltipId += 1;
const invokedContextTooltipId = contextTooltipId
setTimeout(() => {
if (contextTooltipId === invokedContextTooltipId) {
contextTooltipAnchor = e.target;
contextTooltipOption = option;
contextTooltipVisible = true;
} else {
console.log("not long enough");
}
}, 400)
}
const handleMouseleave = (e, option) => {
setTimeout(() => {
if (option === contextTooltipOption) {
contextTooltipVisible = false;
}
}, 600)
}
</script>
<button
@ -158,83 +194,29 @@
customHeight={customPopoverHeight}
>
<div
class="popover-content"
class:auto-width={autoWidth}
use:clickOutside={() => (open = false)}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
<ul class="spectrum-Menu" role="listbox" bind:this={component}>
{#if placeholderOption}
<li
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}
>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
class="popover-content"
class:auto-width={autoWidth}
use:clickOutside={() => (open = false)}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
{#if filteredOptions.length}
{#each filteredOptions as option, idx}
<ul class="spectrum-Menu" role="listbox" bind:this={component}>
{#if placeholderOption}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
class:is-disabled={!isOptionEnabled(option)}
on:click={() => onSelectOption(null)}
>
{#if getOptionIcon(option, idx)}
<span class="option-extra icon">
{#if useoptioniconimage}
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="15"
height="15"
/>
{:else}
<Icon size="S" name={getOptionIcon(option, idx)} />
{/if}
</span>
{/if}
{#if getOptionColour(option, idx)}
<span class="option-extra">
<StatusLight square color={getOptionColour(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span>
{#if option.tag}
<span class="option-tag">
<Tags>
<Tag icon="LockClosed">{option.tag}</Tag>
</Tags>
</span>
{/if}
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
@ -243,25 +225,104 @@
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
{#if filteredOptions.length}
{#each filteredOptions as option, idx}
<li
on:mouseenter={(e) => handleMouseenter(e, option)}
on:mouseleave={(e) => handleMouseleave(e, option)}
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
class:is-disabled={!isOptionEnabled(option)}
>
{#if getOptionIcon(option, idx)}
<span class="option-extra icon">
{#if useOptionIconImage}
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="15"
height="15"
/>
{:else}
<Icon tooltip={getOptionIconTooltip(option)} size="S" name={getOptionIcon(option, idx)} />
{/if}
</span>
{/if}
{#if getOptionColour(option, idx)}
<span class="option-extra">
<StatusLight square color={getOptionColour(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span>
{#if option.tag}
<span class="option-tag">
<Tags>
<Tag icon="LockClosed">{option.tag}</Tag>
</Tags>
</span>
{/if}
<svg
class="selectedIcon spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
</ul>
{#if loading}
<div class="loading" class:loading--withAutocomplete={autocomplete}>
<ProgressCircle size="S" />
</div>
{/if}
</ul>
{#if loading}
<div class="loading" class:loading--withAutocomplete={autocomplete}>
<ProgressCircle size="S" />
</div>
{/if}
{#if footer}
<div class="footer">
{footer}
</div>
{/if}
{#if footer}
<div class="footer">
{footer}
</div>
{/if}
</div>
</Popover>
<ContextTooltip
visible={contextTooltipVisible}
anchor={contextTooltipAnchor}
>
<div
class="tooltipContents"
>
{contextTooltipOption}
</div>
</Popover>
</ContextTooltip>
<style>
.tooltipContents {
width: 300px;
background-color: red;
}
.spectrum-Menu {
display: block;
}
.spectrum-Menu:hover .context {
display: block;
}
.spectrum-Picker {
width: 100%;
box-shadow: none;
@ -332,7 +393,7 @@
right: 2px;
top: 2px;
}
.popover-content :global(.spectrum-Search .spectrum-Textfield-icon) {
.popover-content :global(.spectrum-Search .spectrum-Textfield-icon) {
top: 9px;
}

View File

@ -13,12 +13,17 @@
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionIcon = () => null
export let getOptionIconTooltip = () => null
export let getOptionTooltip = () => null
export let isOptionEnabled = () => true
export let sort = false
export let autoWidth = false
export let autocomplete = false
export let searchTerm = null
export let customPopoverHeight
export let helpText = null
export let align
const dispatch = createEventDispatcher()
const onChange = e => {
@ -29,6 +34,10 @@
<Field {helpText} {label} {labelPosition} {error}>
<Multiselect
{isOptionEnabled}
{getOptionIcon}
{getOptionIconTooltip}
{getOptionTooltip}
{error}
{disabled}
{readonly}
@ -41,6 +50,7 @@
{autoWidth}
{autocomplete}
{customPopoverHeight}
{align}
bind:searchTerm
on:change={onChange}
on:click

View File

@ -0,0 +1,129 @@
<script>
import Portal from "svelte-portal"
export let tooltip
export let anchor
export let visible = false
export let hovering = false
let targetX = 0
let targetY = 0
let animationId = 0;
let x = 0;
let y = 0;
const updatePositionOnVisibilityChange = (visible, hovering) => {
if (!visible && !hovering) {
x = 0;
y = 0;
}
}
const updatePosition = (anchor, tooltip) => {
if (anchor == null) {
return;
}
const rect = anchor.getBoundingClientRect();
const tooltipWidth = tooltip?.getBoundingClientRect()?.width ?? 0;
targetX = rect.x - tooltipWidth
targetY = rect.y
animationId += 1
if (x === 0 && y === 0) {
x = targetX
y = targetY
}
}
const animate = (invokedAnimationId, xRate = null, yRate = null) => {
if (invokedAnimationId !== animationId) {
console.log("CANCEL ANIMATION ", invokedAnimationId, " ", animationId);
return;
}
console.log("animating");
const animationDurationInFrames = 10;
const xDelta = targetX - x
const yDelta = targetY - y
if (xRate === null) {
xRate = Math.abs(xDelta) / animationDurationInFrames
}
if (yRate === null) {
yRate = Math.abs(yDelta) / animationDurationInFrames
}
if (xDelta === 0 && yDelta === 0) return;
if (
(xDelta > 0 && xDelta <= xRate) ||
(xDelta < 0 && xDelta >= -xRate)
) {
x = targetX;
} else if (xDelta > 0) {
x = x + xRate;
} else if (xDelta < 0) {
x = x - xRate;
}
if (
(yDelta > 0 && yDelta <= yRate) ||
(yDelta < 0 && yDelta >= -yRate)
) {
y = targetY;
} else if (yDelta > 0) {
y = y + yRate;
} else if (yDelta < 0) {
y = y - yRate;
}
requestAnimationFrame(() => animate(invokedAnimationId, xRate, yRate))
}
$: updatePosition(anchor, tooltip)
$: updatePositionOnVisibilityChange(visible, hovering)
$: requestAnimationFrame(() => animate(animationId))
const handleMouseenter = (e) => {
hovering = true;
}
const handleMouseleave = (e) => {
hovering = false;
}
</script>
<Portal target=".spectrum">
<div
bind:this={tooltip}
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
style:top={`${y}px`}
style:left={`${x}px`}
class="tooltip"
class:visible={visible || hovering}
>
<slot />
</div>
</Portal>
<style>
.tooltip {
position: absolute;
z-index: 9999;
opacity: 0;
pointer-events: none;
}
.visible {
opacity: 1;
pointer-events: auto;
}
</style>

View File

@ -3,14 +3,36 @@
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import { selectedScreen } from "stores/builder"
import { createEventDispatcher } from "svelte"
import { validators, supported, partialSupport, unsupported } from "../fieldValidator";
export let componentInstance = {}
export let value = ""
export let placeholder
export let fieldValidator
$: {
console.log(fieldValidator);
}
const getFieldSupport = (schema, fieldValidator) => {
if (fieldValidator == null) {
return {}
}
const validator = validators[fieldValidator];
const fieldSupport = {}
Object.entries(schema || {}).forEach(([key, value]) => {
fieldSupport[key] = validator(value)
})
return fieldSupport
}
const dispatch = createEventDispatcher()
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
$: schema = getSchemaForDatasource($selectedScreen, datasource).schema
$: fieldSupport = getFieldSupport(schema, fieldValidator);
$: options = Object.keys(schema || {})
$: boundValue = getValidValue(value, options)
@ -32,6 +54,32 @@
boundValue = getValidValue(value.detail, options)
dispatch("change", boundValue)
}
const getOptionIcon = option => {
const support = fieldSupport[option]?.support;
if (support == null) return null;
if (support === supported) return null
if (support === partialSupport) return "Warning"
if (support === unsupported) return "Error"
}
const getOptionIconTooltip = option => {
}
const isOptionEnabled = option => {
const support = fieldSupport[option]?.support;
if (support == null) return true
if (support == unsupported) return false
return true
}
</script>
<Select {placeholder} value={boundValue} on:change={onChange} {options} />
<Select
{isOptionEnabled}
{getOptionIcon}
{getOptionIconTooltip}
{placeholder} value={boundValue} on:change={onChange} {options} />

View File

@ -3,17 +3,60 @@
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import { selectedScreen } from "stores/builder"
import { createEventDispatcher } from "svelte"
import { validators, supported, partialSupport, unsupported } from "../fieldValidator";
export let componentInstance = {}
export let value = ""
export let placeholder
export let fieldValidator
const TypeIconMap = {
text: "Text",
options: "Dropdown",
datetime: "Date",
barcodeqr: "Camera",
longform: "TextAlignLeft",
array: "Dropdown",
number: "123",
boolean: "Boolean",
attachment: "AppleFiles",
link: "DataCorrelated",
formula: "Calculator",
json: "Brackets",
bigint: "TagBold",
bb_reference: {
user: "User",
users: "UserGroup",
},
}
const getFieldSupport = (schema, fieldValidator) => {
if (fieldValidator == null) {
return {}
}
const validator = validators[fieldValidator];
const fieldSupport = {}
Object.entries(schema || {}).forEach(([key, value]) => {
fieldSupport[key] = validator(value)
})
return fieldSupport
}
const dispatch = createEventDispatcher()
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
$: schema = getSchemaForDatasource($selectedScreen, datasource).schema
$: options = Object.keys(schema || {})
$: fieldSupport = getFieldSupport(schema, fieldValidator);
$: boundValue = getValidOptions(value, options)
$: {
console.log(schema)
}
const getValidOptions = (selectedOptions, allOptions) => {
// Fix the hardcoded default string value
if (!Array.isArray(selectedOptions)) {
@ -26,6 +69,69 @@
boundValue = getValidOptions(value.detail, options)
dispatch("change", boundValue)
}
const foo = () => {
const support = fieldSupport[option]?.support;
if (support == null) return null;
if (support === supported) return null
if (support === partialSupport) return "AlertCircleFilled"
if (support === unsupported) return "AlertCircleFilled"
}
const getOptionIcon = optionKey => {
const option = schema[optionKey]
if (option.autocolumn) {
return "MagicWand"
}
const { type, subtype } = option
const result =
typeof TypeIconMap[type] === "object" && subtype
? TypeIconMap[type][subtype]
: TypeIconMap[type]
return result || "Text"
}
const getOptionIconTooltip = optionKey => {
const option = schema[optionKey]
return option.type;
}
const getOptionTooltip = optionKey => {
console.log(optionKey)
const support = fieldSupport[optionKey]?.support;
const message = fieldSupport[optionKey]?.message;
if (support === unsupported) return message
return null
}
const isOptionEnabled = optionKey => {
// Remain enabled if already selected, so it can be deselected
if (value?.includes(optionKey)) return true
const support = fieldSupport[optionKey]?.support;
if (support == null) return true
if (support === unsupported) return false
return true
}
</script>
<Multiselect {placeholder} value={boundValue} on:change={setValue} {options} />
<Multiselect
iconPosition="right"
{isOptionEnabled}
{getOptionIcon}
{getOptionIconTooltip}
{getOptionTooltip}
{placeholder}
value={boundValue}
on:change={setValue}
{options}
align="right"
/>

View File

@ -2,7 +2,7 @@ export const unsupported = Symbol("values-validator-unsupported")
export const partialSupport = Symbol("values-validator-partial-support")
export const supported = Symbol("values-validator-supported")
const validatorMap = {
export const validators = {
chart: (fieldSchema) => {
if (
fieldSchema.type === "json" ||
@ -14,7 +14,7 @@ const validatorMap = {
) {
return {
support: unsupported,
message: "This field cannot be used as a chart value"
message: `"${fieldSchema.type}" columns cannot be used as a chart value long long long long long long long long long`
}
}
@ -38,5 +38,3 @@ const validatorMap = {
}
}
};
export default validatorMap;

View File

@ -1629,7 +1629,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
@ -1788,7 +1788,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
@ -1941,7 +1941,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
@ -2106,7 +2106,7 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
@ -2235,7 +2235,7 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Data columns",
"key": "valueColumn",
"dependsOn": "dataProvider",
@ -2364,28 +2364,28 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Open column",
"key": "openColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Close column",
"key": "closeColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "High column",
"key": "highColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Low column",
"key": "lowColumn",
"dependsOn": "dataProvider",
@ -2449,7 +2449,7 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
@ -5266,7 +5266,7 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5291,7 +5291,7 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5316,7 +5316,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5363,7 +5363,7 @@
},
"settings": [
{
"type": "chartfield",
"type": "field",
"label": "Value column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5411,7 +5411,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5460,7 +5460,7 @@
"required": true
},
{
"type": "chartmultifield",
"type": "multifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5521,28 +5521,28 @@
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Open column",
"key": "openColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Close column",
"key": "closeColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "High column",
"key": "highColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "chartfield",
"type": "field",
"label": "Low column",
"key": "lowColumn",
"dependsOn": "dataSource",