2021-06-01 16:14:24 +02:00
|
|
|
<script lang="ts">
|
|
|
|
import {locals} from '../../ts/stores.js';
|
2021-11-08 00:24:53 +01:00
|
|
|
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;
|
2021-11-08 00:24:53 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-08 00:24:53 +01:00
|
|
|
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)$/)
|
2021-11-08 00:24:53 +01:00
|
|
|
? +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>
|
|
|
|
|
2021-11-08 00:24:53 +01:00
|
|
|
<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;
|
2021-11-08 01:44:26 +01:00
|
|
|
left: 8px;
|
2021-11-08 00:24:53 +01:00
|
|
|
top: 22px;
|
|
|
|
user-select: none;
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
transition-property: top, font-size;
|
|
|
|
transition-duration: 150ms;
|
|
|
|
transition-timing-function: ease-out;
|
|
|
|
|
|
|
|
cursor: text;
|
|
|
|
}
|
|
|
|
|
2021-11-08 01:44:26 +01:00
|
|
|
.has-icon label {
|
|
|
|
left: 68px;
|
|
|
|
}
|
|
|
|
|
2021-11-08 00:24:53 +01:00
|
|
|
&.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:07:31 +01:00
|
|
|
&:not(.empty), select ~, [type="file"] ~, [type="color"] ~, .focused ~, :focus ~ {
|
2021-11-08 00:24:53 +01:00
|
|
|
.sections label {
|
|
|
|
top: 8px;
|
|
|
|
font-size: 14px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
input,
|
|
|
|
select,
|
|
|
|
select,
|
|
|
|
textarea,
|
|
|
|
.form-display,
|
|
|
|
.textarea-growing-wrapper,
|
|
|
|
.textarea-growing-wrapper:after {
|
|
|
|
display: block;
|
2021-11-08 01:44:26 +01:00
|
|
|
padding: 32px 8px 8px;
|
2021-11-08 00:24:53 +01:00
|
|
|
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;
|
|
|
|
right: 8px;
|
|
|
|
top: 30px;
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-09 18:00:52 +01:00
|
|
|
.has-icon {
|
|
|
|
input,
|
|
|
|
select,
|
|
|
|
select,
|
|
|
|
textarea,
|
|
|
|
.form-display,
|
|
|
|
.textarea-growing-wrapper,
|
|
|
|
.textarea-growing-wrapper:after {
|
|
|
|
margin-left: -60px;
|
|
|
|
padding-left: 68px;
|
|
|
|
}
|
|
|
|
|
|
|
|
textarea {
|
|
|
|
margin-left: -68px;
|
|
|
|
width: calc(100% + 68px);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-08 00:24:53 +01:00
|
|
|
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;
|
|
|
|
margin: 16px auto;
|
|
|
|
|
|
|
|
.time-input {
|
|
|
|
position: relative;
|
|
|
|
flex: 1;
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
label {
|
|
|
|
left: 8px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
> :not(.form-field) {
|
|
|
|
padding: 32px 8px 8px 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}
|
2021-11-08 00:24:53 +01:00
|
|
|
<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}>
|
2021-11-08 01:44:26 +01:00
|
|
|
<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'}
|
2021-11-08 00:24:53 +01:00
|
|
|
<fieldset>
|
|
|
|
<legend>{placeholder}</legend>
|
2021-06-01 16:14:24 +02:00
|
|
|
{#each extraData as f}
|
2021-11-08 00:24:53 +01:00
|
|
|
<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}
|
2021-11-08 00:24:53 +01:00
|
|
|
</fieldset>
|
2021-06-01 16:14:24 +02:00
|
|
|
{:else if type === 'select'}
|
2021-11-08 00:24:53 +01:00
|
|
|
<select name={name} id={fieldId} {...$$restProps} bind:this={input} on:input={handleInput}>
|
2021-06-01 16:14:24 +02:00
|
|
|
{#each extraData as option}
|
2021-11-08 00:24:53 +01:00
|
|
|
<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>
|
2021-11-08 00:24:53 +01:00
|
|
|
<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}>
|
2021-11-08 00:24:53 +01:00
|
|
|
<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>
|
2021-11-08 00:24:53 +01:00
|
|
|
</div>
|
2021-06-01 16:14:24 +02:00
|
|
|
{:else if type === 'checkbox'}
|
2021-11-08 00:24:53 +01:00
|
|
|
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
|
2021-06-01 16:14:24 +02:00
|
|
|
{:else}
|
2021-11-08 00:24:53 +01:00
|
|
|
<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}
|
|
|
|
|
2021-11-08 00:24:53 +01:00
|
|
|
<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}
|
2021-11-08 00:24:53 +01:00
|
|
|
<div class="error">
|
|
|
|
<Icon name="alert-circle"/> {validation.message}</div>
|
2021-06-01 16:14:24 +02:00
|
|
|
{/if}
|
|
|
|
{#if hint}
|
2021-11-08 00:24:53 +01:00
|
|
|
<div class="hint">
|
|
|
|
<Icon name="info"/> {hint}</div>
|
2021-06-01 16:14:24 +02:00
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
|