validation
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
|
||||
|
||||
/**
|
||||
* Throttle execution of a function. Especially useful for rate limiting
|
||||
* execution of handlers on events like resize and scroll.
|
||||
*
|
||||
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
|
||||
* are most useful.
|
||||
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
|
||||
* as-is, to `callback` when the throttled-function is executed.
|
||||
* @param {object} [options] - An object to configure options.
|
||||
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
|
||||
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
|
||||
* one final time after the last throttled-function call. (After the throttled-function has not been called for
|
||||
* `delay` milliseconds, the internal counter is reset).
|
||||
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
|
||||
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
|
||||
* callback will never executed if both noLeading = true and noTrailing = true.
|
||||
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
|
||||
* false (at end), schedule `callback` to execute after `delay` ms.
|
||||
*
|
||||
* @returns {Function} A new, throttled, function.
|
||||
*/
|
||||
export function throttle(delay, callback, options) {
|
||||
const {
|
||||
noTrailing = false,
|
||||
noLeading = false,
|
||||
debounceMode = undefined,
|
||||
} = options || {};
|
||||
/*
|
||||
* After wrapper has stopped being called, this timeout ensures that
|
||||
* `callback` is executed at the proper times in `throttle` and `end`
|
||||
* debounce modes.
|
||||
*/
|
||||
let timeoutID;
|
||||
let cancelled = false;
|
||||
|
||||
// Keep track of the last time `callback` was executed.
|
||||
let lastExec = 0;
|
||||
|
||||
// Function to clear existing timeout
|
||||
function clearExistingTimeout() {
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to cancel next exec
|
||||
function cancel(options) {
|
||||
const { upcomingOnly = false } = options || {};
|
||||
clearExistingTimeout();
|
||||
cancelled = !upcomingOnly;
|
||||
}
|
||||
|
||||
/*
|
||||
* The `wrapper` function encapsulates all of the throttling / debouncing
|
||||
* functionality and when executed will limit the rate at which `callback`
|
||||
* is executed.
|
||||
*/
|
||||
function wrapper(...arguments_) {
|
||||
let self = this;
|
||||
let elapsed = Date.now() - lastExec;
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute `callback` and update the `lastExec` timestamp.
|
||||
function exec() {
|
||||
lastExec = Date.now();
|
||||
callback.apply(self, arguments_);
|
||||
}
|
||||
|
||||
/*
|
||||
* If `debounceMode` is true (at begin) this is used to clear the flag
|
||||
* to allow future `callback` executions.
|
||||
*/
|
||||
function clear() {
|
||||
timeoutID = undefined;
|
||||
}
|
||||
|
||||
if (!noLeading && debounceMode && !timeoutID) {
|
||||
/*
|
||||
* Since `wrapper` is being called for the first time and
|
||||
* `debounceMode` is true (at begin), execute `callback`
|
||||
* and noLeading != true.
|
||||
*/
|
||||
exec();
|
||||
}
|
||||
|
||||
clearExistingTimeout();
|
||||
|
||||
if (debounceMode === undefined && elapsed > delay) {
|
||||
if (noLeading) {
|
||||
/*
|
||||
* In throttle mode with noLeading, if `delay` time has
|
||||
* been exceeded, update `lastExec` and schedule `callback`
|
||||
* to execute after `delay` ms.
|
||||
*/
|
||||
lastExec = Date.now();
|
||||
if (!noTrailing) {
|
||||
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
|
||||
* `callback`.
|
||||
*/
|
||||
exec();
|
||||
}
|
||||
} else if (noTrailing !== true) {
|
||||
/*
|
||||
* In trailing throttle mode, since `delay` time has not been
|
||||
* exceeded, schedule `callback` to execute `delay` ms after most
|
||||
* recent execution.
|
||||
*
|
||||
* If `debounceMode` is true (at begin), schedule `clear` to execute
|
||||
* after `delay` ms.
|
||||
*
|
||||
* If `debounceMode` is false (at end), schedule `callback` to
|
||||
* execute after `delay` ms.
|
||||
*/
|
||||
timeoutID = setTimeout(
|
||||
debounceMode ? clear : exec,
|
||||
debounceMode === undefined ? delay - elapsed : delay,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.cancel = cancel;
|
||||
|
||||
// Return the wrapper function.
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export function debounce(delay, callback, options) {
|
||||
const { atBegin = false } = options || {};
|
||||
return throttle(delay, callback, { debounceMode: atBegin !== false });
|
||||
}
|
||||
@@ -82,6 +82,10 @@
|
||||
Developers will use this to reference the field
|
||||
</small>
|
||||
</label>
|
||||
<label>
|
||||
Help text
|
||||
<input bind:value={data.field.help} />
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={data.field.translatable}
|
||||
@@ -90,6 +94,32 @@
|
||||
/>
|
||||
Is Translatable
|
||||
</label>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={data.field.required}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Required
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={data.field.readonly}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Readonly
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={data.field.hidden}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Hidden
|
||||
</label>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
{#if data.field.type === "text"}
|
||||
|
||||
@@ -2,41 +2,12 @@
|
||||
let { field } = $props();
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={field.props.required}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Required
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={field.props.readonly}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Readonly
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
bind:checked={field.props.hidden}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
Hidden
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
Default
|
||||
<input bind:value={field.props.default} />
|
||||
</label>
|
||||
<label>
|
||||
Help text
|
||||
<input bind:value={field.props.help} />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Min characters
|
||||
<input type="number" bind:value={field.props.min} />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import { getSelectedLocales, getLocaleName } from "./locale.svelte.js";
|
||||
let { channel, onLocaleChange } = $props();
|
||||
let selectedLocales = $state(getSelectedLocales());
|
||||
|
||||
let selectedLocaleNames = $derived(
|
||||
selectedLocales.map((id) => getLocaleName(channel, id)),
|
||||
);
|
||||
function handleChange() {
|
||||
localStorage.setItem("selectedLocales", selectedLocales);
|
||||
onLocaleChange();
|
||||
}
|
||||
</script>
|
||||
|
||||
<details class="dropdown">
|
||||
<summary>Locales: {selectedLocaleNames.join(", ")}</summary>
|
||||
<ul>
|
||||
{#each channel.locales as locale}
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={selectedLocales}
|
||||
onchange={handleChange}
|
||||
value={locale.id}
|
||||
/>
|
||||
{locale.name}
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import TextField from "./fields/TextField.svelte";
|
||||
import LocaleChooser from "./LocaleChooser.svelte";
|
||||
import { getSelectedLocales } from "./locale.svelte.js";
|
||||
let { channel, user, data } = $props();
|
||||
let selectedLocales = $state(getSelectedLocales());
|
||||
let record = $state(data.record);
|
||||
|
||||
function handleLocaleChange() {
|
||||
selectedLocales = getSelectedLocales();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <svelte:window on:beforeunload={beforeUnload} /> -->
|
||||
<ChannelLayout {body} {channel} schemas={data.schemas} {user}></ChannelLayout>
|
||||
{#snippet body()}
|
||||
<LocaleChooser {channel} onLocaleChange={handleLocaleChange}
|
||||
></LocaleChooser>
|
||||
{#each data.fields as field}
|
||||
<div style="display:flex;gap:20px;">
|
||||
{#if field.type === "text"}
|
||||
<TextField
|
||||
{channel}
|
||||
{record}
|
||||
errors={data.validationErrors.filter(
|
||||
(f) => f.id === field.fieldId && f.locale === "main",
|
||||
)}
|
||||
schemaField={field}
|
||||
locale="main"
|
||||
dataField={data.draftData.find(
|
||||
(f) => f.id === field.id && f.locale === "main",
|
||||
)}
|
||||
></TextField>
|
||||
{#if field.translatable}
|
||||
{#each selectedLocales as locale (locale)}
|
||||
<TextField
|
||||
{channel}
|
||||
{record}
|
||||
errors={data.validationErrors.filter(
|
||||
(f) =>
|
||||
f.fieldId === field.id &&
|
||||
f.locale === locale,
|
||||
)}
|
||||
schemaField={field}
|
||||
{locale}
|
||||
dataField={data.draftData.find(
|
||||
(f) => f.id === field.id && f.locale === locale,
|
||||
)}
|
||||
></TextField>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
@@ -0,0 +1,132 @@
|
||||
<script>
|
||||
import { post } from "../../../modules/remote";
|
||||
import { getApp } from "../../../app";
|
||||
import { getLocaleName } from "../locale.svelte.js";
|
||||
let { channel, record, schemaField, dataField, locale, errors } = $props();
|
||||
let originalValue = dataField?.value ?? schemaField.props.default;
|
||||
let newValue = $state(originalValue);
|
||||
let valuesChanged = $derived(newValue !== originalValue);
|
||||
let errorMessage = $state("");
|
||||
let validationErrors = $state(errors);
|
||||
let hasErrors = $derived(validationErrors.length > 0);
|
||||
const app = getApp();
|
||||
|
||||
function save() {
|
||||
if (!valuesChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
post(
|
||||
app.url("records/fields"),
|
||||
{
|
||||
recordId: record.id,
|
||||
id: schemaField.id,
|
||||
locale: locale,
|
||||
value: newValue,
|
||||
},
|
||||
(data, err) => {
|
||||
if (err.isNotEmpty()) {
|
||||
errorMessage = err.first();
|
||||
} else {
|
||||
validationErrors = data.validationErrors;
|
||||
originalValue = newValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
let delayMs = 1000;
|
||||
let timer = $state(undefined);
|
||||
let isComposing = $state(false);
|
||||
|
||||
const schedule = () => {
|
||||
if (isComposing) {
|
||||
return;
|
||||
}
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(flush, delayMs);
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
// value = inputValue;
|
||||
save();
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
schedule();
|
||||
};
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
flush();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
flush();
|
||||
};
|
||||
|
||||
const handleCompositionStart = () => {
|
||||
isComposing = true;
|
||||
};
|
||||
|
||||
const handleCompositionEnd = () => {
|
||||
isComposing = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- <div style="position: relative;">
|
||||
{#if field.selectOptions}
|
||||
<Autocomplete {field} bind:value></Autocomplete>
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
{id}
|
||||
class="form-control"
|
||||
class:is-invalid={errorMessage}
|
||||
bind:value
|
||||
autocomplete="off"
|
||||
readonly={field.readonly && !isCreateMode}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div> -->
|
||||
<div style="min-width: 400px;">
|
||||
<label>
|
||||
{#if locale !== "main"}
|
||||
{getLocaleName(channel, locale)} >
|
||||
{/if}
|
||||
{schemaField.name} <br />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newValue}
|
||||
autocomplete="off"
|
||||
readonly={schemaField.readonly}
|
||||
aria-describedby={schemaField.id + "-help"}
|
||||
oninput={handleInput}
|
||||
onkeydown={handleKeydown}
|
||||
onblur={handleBlur}
|
||||
oncompositionstart={handleCompositionStart}
|
||||
oncompositionend={handleCompositionEnd}
|
||||
aria-invalid={hasErrors ? "true" : ""}
|
||||
/>
|
||||
{#if hasErrors}
|
||||
<small id={schemaField.id + "-help"}
|
||||
>{validationErrors[0].message}</small
|
||||
>
|
||||
{:else if schemaField.help != ""}
|
||||
<small id={schemaField.id + "-help"}>{schemaField.help}</small>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
export function getSelectedLocales() {
|
||||
let value = $state(localStorage.getItem("selectedLocales"));
|
||||
if (value == "" || !value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(",");
|
||||
}
|
||||
|
||||
export function getLocaleName(channel, id) {
|
||||
return channel.locales.find((locale) => locale.id === id).name;
|
||||
}
|
||||
+2
-2
@@ -9,7 +9,7 @@ import Profile from "./svelte/account/Profile.svelte";
|
||||
import SetupEntry from "./entry/SetupEntry/SetupEntry.svelte";
|
||||
import Members from "./svelte/members/Members.svelte";
|
||||
import RecordNotFound from "./svelte/records/NotFound.svelte";
|
||||
import RecordEdit from "./svelte/records/Edit.svelte";
|
||||
import RecordEditEntry from "./entry/RecordEditEntry/RecordEditEntry.svelte";
|
||||
import ContentEntry from "./entry/ContentEntry/ContentEntry.svelte";
|
||||
import HomeEntry from "./entry/HomeEntry/HomeEntry.svelte";
|
||||
import SchemaEntry from "./entry/SchemaEntry/SchemaEntry.svelte";
|
||||
@@ -21,7 +21,7 @@ import { createApp } from "./app";
|
||||
|
||||
const entryComponents = {
|
||||
members: Members,
|
||||
recordEdit: RecordEdit,
|
||||
recordEdit: RecordEditEntry,
|
||||
recordNotFound: RecordNotFound,
|
||||
contentIndex: ContentEntry,
|
||||
homeIndex: HomeEntry,
|
||||
|
||||
Reference in New Issue
Block a user