swaf/src/assets/views/utils/Field.svelte

460 lines
11 KiB
Svelte
Raw Normal View History

2021-06-01 16:14:24 +02:00
<script lang="ts">
import {locals} from '../../ts/stores.js';
import {FileSize} from "../../../common/FileSize.js";
2021-06-01 16:14:24 +02:00
import Message from "../components/Message.svelte";
import Icon from "./Icon.svelte";
import {getContext} from "svelte";
export let type: string;
export let name: string;
type FieldValue = string | number | Record<string, FieldValue>;
export let value: FieldValue | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let hint: string | undefined = undefined;
export let extraData: string[] | undefined = undefined;
export let icon: string | undefined = undefined;
export let validation: { message: string, value?: string } | undefined = $locals.validation()?.[name];
2021-06-01 16:14:24 +02:00
const formId = getContext('formId');
const fieldId = `${formId}-${name}-field`;
const previousFormData = $locals.previousFormData() || [];
value = type !== 'hidden' && previousFormData[name] || value || validation?.value || '';
function durationValue(f: string): number {
if (previousFormData[name]) {
return value[f];
}
switch (f) {
case 's':
return value % 60;
case 'm':
return (value - value % 60) / 60 % 60;
case 'h':
return (value - value % 3600) / 3600;
default:
return 0;
}
}
function focusInput() {
if (input) {
if (['file', 'checkbox', 'color'].indexOf(input.type) >= 0) {
input.click();
} else {
input.focus();
}
} else {
this.querySelector('input')?.focus();
}
}
function handleInput() {
2021-06-01 16:14:24 +02:00
// in here, you can switch on type and implement
// whatever behaviour you need
value = type.match(/^(number|range)$/)
? +this.value
: this.value;
if (this.type === 'file') {
handleFileInput();
}
}
let input: HTMLInputElement;
function chooseFile() {
input.click();
}
let files: FileList | undefined;
function handleFileInput() {
files = input.files;
2021-06-01 16:14:24 +02:00
}
2021-11-09 18:07:31 +01:00
let focused = false;
2021-06-01 16:14:24 +02:00
</script>
<style lang="scss">
@import "../../scss/helpers";
.form-field:not(.hidden) {
display: flex;
flex-direction: column;
margin: 16px auto;
.control {
position: relative;
display: flex;
align-items: start;
flex-direction: row;
color: var(--on-input);
background-color: var(--input);
border-radius: 5px;
> :global(.feather.icon) {
--icon-size: 24px;
margin: 18px;
opacity: 0.75;
}
}
label {
position: absolute;
z-index: 1;
left: 8px;
top: 22px;
user-select: none;
font-size: 16px;
transition-property: top, font-size;
transition-duration: 150ms;
transition-timing-function: ease-out;
cursor: text;
}
.has-icon label {
left: 68px;
}
&.disabled {
opacity: 0.5;
&, * {
cursor: not-allowed;
}
}
input, select, textarea {
z-index: 1;
border: 0;
color: inherit;
background: transparent;
font-size: 16px;
border-radius: 5px;
outline-offset: 0;
}
2021-11-09 18:34:01 +01:00
&:not(.empty),
select ~,
[type="file"] ~,
[type="color"] ~,
[type="number"] ~,
.focused ~,
:focus ~,
fieldset {
.sections label, legend, .time-input label {
top: 8px;
font-size: 14px;
}
}
input,
select,
select,
textarea,
.form-display,
.textarea-growing-wrapper,
.textarea-growing-wrapper:after {
display: block;
padding: 32px 8px 8px;
width: 100%;
height: 60px;
}
select {
position: relative;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
&::-ms-expand {
display: none;
}
& + :global(.feather) {
position: absolute;
pointer-events: none;
2021-11-09 18:34:01 +01:00
right: 0;
top: 0;
transition: transform 150ms ease-out;
}
// TODO: Temporary
&:focus + :global(.feather) {
transform: rotateX(180deg);
}
}
.textarea-growing-wrapper {
display: grid;
flex-grow: 1;
height: revert;
padding: 0;
&:after {
content: attr(data-value) " ";
color: red;
box-sizing: border-box;
font: inherit;
visibility: hidden;
}
textarea {
height: auto;
resize: none;
font-family: inherit;
overflow: hidden;
}
&:after, textarea {
grid-area: 1 / 1 / 2 / 2;
margin-left: revert;
min-height: 100px;
height: revert;
white-space: pre-wrap;
word-wrap: anywhere;
}
}
.has-icon > {
input,
select,
select,
2021-11-09 18:34:01 +01:00
fieldset,
.form-display,
.textarea-growing-wrapper,
.textarea-growing-wrapper:after,
.textarea-growing-wrapper > textarea {
margin-left: -60px;
padding-left: 68px;
}
.textarea-growing-wrapper > textarea {
margin-left: -68px;
width: calc(100% + 68px);
}
}
input[type=color] {
height: calc(32px + 8px + 32px);
}
&.checkbox, &.color, &.file {
input, label {
cursor: pointer;
}
}
&.checkbox {
.control {
display: flex;
flex-direction: row;
align-items: stretch;
flex-grow: 1;
}
input {
width: 20px;
height: 20px;
margin: auto 8px;
text-align: left;
& ~ .sections {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
label {
position: static;
font-size: 16px;
}
}
}
}
&.file {
input {
@include fake-hide;
}
label {
position: static;
display: block;
padding: 8px;
font-size: revert !important;
}
}
.files {
display: flex;
flex-direction: column;
align-items: start;
.file {
margin: 8px;
padding: 8px;
color: var(--on-primary);
background: var(--primary);
border-radius: 5px;
> div {
display: inline-block;
padding: 8px;
}
.name {
font-size: 20px;
:global(.feather) {
--icon-size: 24px;
margin-right: 8px;
}
}
}
}
fieldset {
display: flex;
flex-direction: row;
align-items: start;
2021-11-09 18:34:01 +01:00
width: 100%;
margin: 0;
padding-top: 8px;
border: 0;
legend {
padding: 8px;
}
.time-input {
position: relative;
flex: 1;
margin: 0;
label {
left: 8px;
}
}
+ {
.error, .hint {
margin-top: -16px;
margin-bottom: 16px;
}
}
}
}
.form-field, fieldset + {
.error, .hint {
padding: 2px 2px 2px 4px;
text-align: left;
font-size: 14px;
color: var(--color);
}
}
</style>
2021-06-01 16:14:24 +02:00
{#if type === 'hidden'}
{#if validation}
<Message type="error" content={validation.message}/>
{/if}
<input type="hidden" name={name} value={value}>
{:else}
<div class="form-field"
class:checkbox={type === 'checkbox'}
class:color={type === 'color'}
class:file={type === 'file'}
class:empty={value === ''}
class:disabled={Object.keys($$restProps).indexOf('disabled') >= 0}>
<div class="control" class:has-icon={icon} on:click={focusInput}>
2021-06-01 16:14:24 +02:00
{#if icon}
<Icon name={icon}/>
{/if}
{#if type === 'duration'}
<fieldset>
<legend>{placeholder}</legend>
2021-06-01 16:14:24 +02:00
{#each extraData as f}
<div class="time-input">
<input type="number" name="{name}[{f}]" id="{fieldId}-{f}"
value={durationValue(f)}
min="0" max={(f === 's' || f === 'm') && '60' || undefined}
{...$$restProps} on:click={e => e.stopPropagation()}>
<label for="{fieldId}-{f}" on:click={e => e.stopPropagation()}>{f}</label>
</div>
2021-06-01 16:14:24 +02:00
{/each}
</fieldset>
2021-06-01 16:14:24 +02:00
{:else if type === 'select'}
<select name={name} id={fieldId} {...$$restProps} bind:this={input} on:input={handleInput}>
2021-06-01 16:14:24 +02:00
{#each extraData as option}
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
selected={value === (option.value || option)}>{option.display || option}</option>
2021-06-01 16:14:24 +02:00
{/each}
</select>
<Icon name="chevron-down"/>
2021-06-01 16:14:24 +02:00
{:else if type === 'textarea'}
2021-11-09 18:07:31 +01:00
<div class="textarea-growing-wrapper" class:focused={focused} data-value={value}>
<textarea {name} id={fieldId} {value} {...$$restProps} bind:this={input}
2021-11-09 18:07:31 +01:00
on:input={handleInput}
on:focusin={() => focused = true}
on:focusout={() => focused = false}></textarea>
</div>
2021-06-01 16:14:24 +02:00
{:else if type === 'checkbox'}
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
2021-06-01 16:14:24 +02:00
{:else}
<input {type} {name} id={fieldId} {value} {...$$restProps} bind:this={input} on:input={handleInput}
tabindex={type === 'file' ? '-1' : undefined}>
2021-06-01 16:14:24 +02:00
{/if}
<div class="sections">
{#if type !== 'duration'}
<label for={fieldId}>{@html placeholder || ''}
<slot/>
</label>
{/if}
{#if type === 'file'}
{#if files}
<div class="files">
{#each files as file}
<div class="file">
<div class="name" title="Type: {file.type}">
<Icon name="file"/> {file.name}
</div>
<div class="size" title="{file.size} bytes">
{FileSize.humanizeFileSize(file.size, true)}
</div>
</div>
{/each}
</div>
{/if}
<button type="button" on:click={chooseFile}>Browse...</button>
{/if}
</div>
2021-06-01 16:14:24 +02:00
</div>
{#if validation}
<div class="error">
<Icon name="alert-circle"/> {validation.message}</div>
2021-06-01 16:14:24 +02:00
{/if}
{#if hint}
<div class="hint">
<Icon name="info"/> {hint}</div>
2021-06-01 16:14:24 +02:00
{/if}
</div>
{/if}