@marianmeres/stuic 3.45.3 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CronInput/CronInput.svelte +517 -0
- package/dist/components/CronInput/CronInput.svelte.d.ts +56 -0
- package/dist/components/CronInput/cron-next-run.svelte.d.ts +18 -0
- package/dist/components/CronInput/cron-next-run.svelte.js +62 -0
- package/dist/components/CronInput/index.css +221 -0
- package/dist/components/CronInput/index.d.ts +2 -0
- package/dist/components/CronInput/index.js +2 -0
- package/dist/components/Tree/Tree.svelte +661 -0
- package/dist/components/Tree/Tree.svelte.d.ts +95 -0
- package/dist/components/Tree/index.css +186 -0
- package/dist/components/Tree/index.d.ts +2 -0
- package/dist/components/Tree/index.js +1 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +2 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +4 -2
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
|
|
6
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
7
|
+
|
|
8
|
+
export interface CronPreset {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type CronInputMode = "predefined" | "manual";
|
|
14
|
+
|
|
15
|
+
export interface Props {
|
|
16
|
+
// Core
|
|
17
|
+
value?: string;
|
|
18
|
+
el?: HTMLElement;
|
|
19
|
+
id?: string;
|
|
20
|
+
|
|
21
|
+
// Mode toggle (overrides show* flags when defined)
|
|
22
|
+
mode?: CronInputMode;
|
|
23
|
+
|
|
24
|
+
// InputWrap standard props
|
|
25
|
+
label?: SnippetWithId | THC;
|
|
26
|
+
description?: SnippetWithId | THC;
|
|
27
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
31
|
+
class?: string;
|
|
32
|
+
style?: string;
|
|
33
|
+
|
|
34
|
+
// InputWrap layout
|
|
35
|
+
labelLeft?: boolean;
|
|
36
|
+
labelLeftWidth?: "normal" | "wide";
|
|
37
|
+
labelLeftBreakpoint?: number;
|
|
38
|
+
labelAfter?: SnippetWithId | THC;
|
|
39
|
+
below?: SnippetWithId | THC;
|
|
40
|
+
|
|
41
|
+
// CronInput-specific
|
|
42
|
+
showPresets?: boolean;
|
|
43
|
+
showFields?: boolean;
|
|
44
|
+
showRawInput?: boolean;
|
|
45
|
+
showDescription?: boolean;
|
|
46
|
+
showNextRun?: boolean;
|
|
47
|
+
presets?: CronPreset[];
|
|
48
|
+
onchange?: (expression: string, valid: boolean) => void;
|
|
49
|
+
|
|
50
|
+
// Sub-element class overrides
|
|
51
|
+
classLabel?: string;
|
|
52
|
+
classLabelBox?: string;
|
|
53
|
+
classInputBox?: string;
|
|
54
|
+
classInputBoxWrap?: string;
|
|
55
|
+
classInputBoxWrapInvalid?: string;
|
|
56
|
+
classDescBox?: string;
|
|
57
|
+
classBelowBox?: string;
|
|
58
|
+
classFields?: string;
|
|
59
|
+
classField?: string;
|
|
60
|
+
classFieldLabel?: string;
|
|
61
|
+
classFieldInput?: string;
|
|
62
|
+
classPreset?: string;
|
|
63
|
+
classRaw?: string;
|
|
64
|
+
classSummary?: string;
|
|
65
|
+
classToggleButton?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_PRESETS: CronPreset[] = [
|
|
69
|
+
{ label: "Every minute", value: "* * * * *" },
|
|
70
|
+
{ label: "Every 5 minutes", value: "*/5 * * * *" },
|
|
71
|
+
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
|
72
|
+
{ label: "Every 30 minutes", value: "*/30 * * * *" },
|
|
73
|
+
{ label: "Every hour", value: "0 * * * *" },
|
|
74
|
+
{ label: "Every 6 hours", value: "0 */6 * * *" },
|
|
75
|
+
{ label: "Daily at midnight", value: "0 0 * * *" },
|
|
76
|
+
{ label: "Daily at noon", value: "0 12 * * *" },
|
|
77
|
+
{ label: "Weekdays at 9:00", value: "0 9 * * 1-5" },
|
|
78
|
+
{ label: "Weekly (Sunday midnight)", value: "0 0 * * 0" },
|
|
79
|
+
{ label: "Monthly (1st at midnight)", value: "0 0 1 * *" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const FIELD_DEFS = [
|
|
83
|
+
{ key: "minute", label: "Min", placeholder: "0-59" },
|
|
84
|
+
{ key: "hour", label: "Hour", placeholder: "0-23" },
|
|
85
|
+
{ key: "dayOfMonth", label: "Day", placeholder: "1-31" },
|
|
86
|
+
{ key: "month", label: "Month", placeholder: "1-12" },
|
|
87
|
+
{ key: "dayOfWeek", label: "Wday", placeholder: "0-6" },
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
const MONTH_NAMES = [
|
|
91
|
+
"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
92
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
96
|
+
|
|
97
|
+
type FieldKey = (typeof FIELD_DEFS)[number]["key"];
|
|
98
|
+
|
|
99
|
+
function cronToHuman(expression: string): string {
|
|
100
|
+
const parts = expression.trim().split(/\s+/);
|
|
101
|
+
if (parts.length !== 5) return "";
|
|
102
|
+
|
|
103
|
+
const [minute, hour, dom, month, dow] = parts;
|
|
104
|
+
const segments: string[] = [];
|
|
105
|
+
|
|
106
|
+
// Minute
|
|
107
|
+
if (minute === "*") {
|
|
108
|
+
segments.push("every minute");
|
|
109
|
+
} else if (minute.startsWith("*/")) {
|
|
110
|
+
segments.push(`every ${minute.slice(2)} minutes`);
|
|
111
|
+
} else {
|
|
112
|
+
segments.push(`at minute ${minute}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Hour
|
|
116
|
+
if (hour === "*") {
|
|
117
|
+
// implied by "every minute/N minutes"
|
|
118
|
+
} else if (hour.startsWith("*/")) {
|
|
119
|
+
segments.push(`every ${hour.slice(2)} hours`);
|
|
120
|
+
} else {
|
|
121
|
+
segments.push(`at ${hour.padStart(2, "0")}:${minute === "*" ? "00" : minute.padStart(2, "0")}`);
|
|
122
|
+
// remove the minute segment if we have a specific hour
|
|
123
|
+
if (minute !== "*" && !minute.includes("/")) {
|
|
124
|
+
segments.length = 0;
|
|
125
|
+
segments.push(`at ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Day of month
|
|
130
|
+
if (dom !== "*") {
|
|
131
|
+
if (dom.includes("-")) {
|
|
132
|
+
segments.push(`on days ${dom} of the month`);
|
|
133
|
+
} else if (dom.includes(",")) {
|
|
134
|
+
segments.push(`on days ${dom} of the month`);
|
|
135
|
+
} else {
|
|
136
|
+
const n = parseInt(dom);
|
|
137
|
+
const suffix = n === 1 ? "st" : n === 2 ? "nd" : n === 3 ? "rd" : "th";
|
|
138
|
+
segments.push(`on the ${n}${suffix}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Month
|
|
143
|
+
if (month !== "*") {
|
|
144
|
+
if (month.includes("-")) {
|
|
145
|
+
const [s, e] = month.split("-").map(Number);
|
|
146
|
+
segments.push(`in ${MONTH_NAMES[s]}-${MONTH_NAMES[e]}`);
|
|
147
|
+
} else if (month.includes(",")) {
|
|
148
|
+
const names = month.split(",").map((m) => MONTH_NAMES[parseInt(m)] || m);
|
|
149
|
+
segments.push(`in ${names.join(", ")}`);
|
|
150
|
+
} else {
|
|
151
|
+
segments.push(`in ${MONTH_NAMES[parseInt(month)] || month}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Day of week
|
|
156
|
+
if (dow !== "*") {
|
|
157
|
+
if (dow.includes("-")) {
|
|
158
|
+
const [s, e] = dow.split("-").map(Number);
|
|
159
|
+
segments.push(`on ${DAY_NAMES[s]}-${DAY_NAMES[e]}`);
|
|
160
|
+
} else if (dow.includes(",")) {
|
|
161
|
+
const names = dow.split(",").map((d) => DAY_NAMES[parseInt(d)] || d);
|
|
162
|
+
segments.push(`on ${names.join(", ")}`);
|
|
163
|
+
} else {
|
|
164
|
+
segments.push(`on ${DAY_NAMES[parseInt(dow)] || dow}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return segments.length ? segments[0].charAt(0).toUpperCase() + segments.join(", ").slice(1) : "";
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<script lang="ts">
|
|
173
|
+
import { CronParser } from "@marianmeres/cron";
|
|
174
|
+
import {
|
|
175
|
+
validate as validateAction,
|
|
176
|
+
type ValidationResult,
|
|
177
|
+
} from "../../actions/validate.svelte.js";
|
|
178
|
+
import { tooltip } from "../../actions/index.js";
|
|
179
|
+
import { iconList, iconSlidersHorizontal } from "../../icons/index.js";
|
|
180
|
+
import { getId } from "../../utils/get-id.js";
|
|
181
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
182
|
+
import InputWrap from "../Input/_internal/InputWrap.svelte";
|
|
183
|
+
|
|
184
|
+
let {
|
|
185
|
+
value = $bindable("* * * * *"),
|
|
186
|
+
el = $bindable(),
|
|
187
|
+
id = getId(),
|
|
188
|
+
//
|
|
189
|
+
mode = $bindable<CronInputMode | undefined>("predefined"),
|
|
190
|
+
//
|
|
191
|
+
label,
|
|
192
|
+
description,
|
|
193
|
+
renderSize = "md",
|
|
194
|
+
required = false,
|
|
195
|
+
disabled = false,
|
|
196
|
+
validate,
|
|
197
|
+
class: classProp,
|
|
198
|
+
style,
|
|
199
|
+
//
|
|
200
|
+
labelLeft = false,
|
|
201
|
+
labelLeftWidth = "normal",
|
|
202
|
+
labelLeftBreakpoint = 480,
|
|
203
|
+
labelAfter,
|
|
204
|
+
below,
|
|
205
|
+
//
|
|
206
|
+
showPresets = true,
|
|
207
|
+
showFields = true,
|
|
208
|
+
showRawInput = true,
|
|
209
|
+
showDescription = true,
|
|
210
|
+
showNextRun = true,
|
|
211
|
+
presets,
|
|
212
|
+
onchange,
|
|
213
|
+
//
|
|
214
|
+
classLabel,
|
|
215
|
+
classLabelBox,
|
|
216
|
+
classInputBox,
|
|
217
|
+
classInputBoxWrap,
|
|
218
|
+
classInputBoxWrapInvalid,
|
|
219
|
+
classDescBox,
|
|
220
|
+
classBelowBox,
|
|
221
|
+
classFields,
|
|
222
|
+
classField,
|
|
223
|
+
classFieldLabel,
|
|
224
|
+
classFieldInput,
|
|
225
|
+
classPreset,
|
|
226
|
+
classRaw,
|
|
227
|
+
classSummary,
|
|
228
|
+
classToggleButton,
|
|
229
|
+
}: Props = $props();
|
|
230
|
+
|
|
231
|
+
// Mode toggle
|
|
232
|
+
const hasModeToggle = $derived(mode !== undefined);
|
|
233
|
+
|
|
234
|
+
function toggleMode() {
|
|
235
|
+
if (mode === "predefined") mode = "manual";
|
|
236
|
+
else mode = "predefined";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Effective show flags — mode overrides explicit props when defined
|
|
240
|
+
let _showPresets = $derived(
|
|
241
|
+
hasModeToggle ? mode === "predefined" : showPresets
|
|
242
|
+
);
|
|
243
|
+
let _showFields = $derived(
|
|
244
|
+
hasModeToggle ? mode === "manual" : showFields
|
|
245
|
+
);
|
|
246
|
+
let _showRawInput = $derived(
|
|
247
|
+
hasModeToggle ? false : showRawInput
|
|
248
|
+
);
|
|
249
|
+
let _showDescription = $derived(
|
|
250
|
+
hasModeToggle ? mode === "manual" : showDescription
|
|
251
|
+
);
|
|
252
|
+
let _showNextRun = $derived(
|
|
253
|
+
hasModeToggle ? mode === "manual" : showNextRun
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const BTN_CLS = [
|
|
257
|
+
"toggle-btn",
|
|
258
|
+
"px-2 rounded-r block",
|
|
259
|
+
"min-w-[44px] min-h-[44px]",
|
|
260
|
+
"flex items-center justify-center",
|
|
261
|
+
].join(" ");
|
|
262
|
+
|
|
263
|
+
// Internal field state
|
|
264
|
+
let fields = $state<Record<FieldKey, string>>({
|
|
265
|
+
minute: "*",
|
|
266
|
+
hour: "*",
|
|
267
|
+
dayOfMonth: "*",
|
|
268
|
+
month: "*",
|
|
269
|
+
dayOfWeek: "*",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
let rawValue = $state("* * * * *");
|
|
273
|
+
let selectedPreset = $state("");
|
|
274
|
+
let error = $state("");
|
|
275
|
+
|
|
276
|
+
// Validation integration
|
|
277
|
+
let validation: ValidationResult | undefined = $state();
|
|
278
|
+
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
279
|
+
|
|
280
|
+
// Track sync source to avoid loops
|
|
281
|
+
let _syncSource: "value" | "fields" | "raw" | "preset" | "" = "";
|
|
282
|
+
|
|
283
|
+
const _presets = $derived(presets ?? DEFAULT_PRESETS);
|
|
284
|
+
|
|
285
|
+
// Combine fields into expression
|
|
286
|
+
function fieldsToExpression(): string {
|
|
287
|
+
return FIELD_DEFS.map((d) => fields[d.key] || "*").join(" ");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Split expression into fields
|
|
291
|
+
function expressionToFields(expr: string) {
|
|
292
|
+
const parts = expr.trim().split(/\s+/);
|
|
293
|
+
if (parts.length === 5) {
|
|
294
|
+
FIELD_DEFS.forEach((d, i) => {
|
|
295
|
+
fields[d.key] = parts[i];
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Match current expression to a preset
|
|
301
|
+
function matchPreset(expr: string): string {
|
|
302
|
+
const normalized = expr.trim().replace(/\s+/g, " ");
|
|
303
|
+
return _presets.find((p) => p.value === normalized)?.value ?? "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Validate expression using CronParser
|
|
307
|
+
function validateExpression(expr: string): boolean {
|
|
308
|
+
try {
|
|
309
|
+
new CronParser(expr);
|
|
310
|
+
error = "";
|
|
311
|
+
validation = undefined;
|
|
312
|
+
return true;
|
|
313
|
+
} catch (e: any) {
|
|
314
|
+
error = e.message || "Invalid cron expression";
|
|
315
|
+
validation = { valid: false, message: error };
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Sync external value → internal state
|
|
321
|
+
$effect(() => {
|
|
322
|
+
const v = value;
|
|
323
|
+
if (_syncSource === "fields" || _syncSource === "raw" || _syncSource === "preset") {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
_syncSource = "value";
|
|
327
|
+
expressionToFields(v);
|
|
328
|
+
rawValue = v;
|
|
329
|
+
selectedPreset = matchPreset(v);
|
|
330
|
+
validateExpression(v);
|
|
331
|
+
_syncSource = "";
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Handle field input changes
|
|
335
|
+
function onFieldInput() {
|
|
336
|
+
_syncSource = "fields";
|
|
337
|
+
const expr = fieldsToExpression();
|
|
338
|
+
rawValue = expr;
|
|
339
|
+
selectedPreset = matchPreset(expr);
|
|
340
|
+
const valid = validateExpression(expr);
|
|
341
|
+
if (valid) {
|
|
342
|
+
value = expr;
|
|
343
|
+
}
|
|
344
|
+
onchange?.(expr, valid);
|
|
345
|
+
_syncSource = "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Handle raw input changes
|
|
349
|
+
function onRawInput() {
|
|
350
|
+
_syncSource = "raw";
|
|
351
|
+
const expr = rawValue;
|
|
352
|
+
expressionToFields(expr);
|
|
353
|
+
selectedPreset = matchPreset(expr);
|
|
354
|
+
const valid = validateExpression(expr);
|
|
355
|
+
if (valid) {
|
|
356
|
+
value = expr;
|
|
357
|
+
}
|
|
358
|
+
onchange?.(expr, valid);
|
|
359
|
+
_syncSource = "";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Handle preset selection
|
|
363
|
+
function onPresetChange() {
|
|
364
|
+
if (!selectedPreset) return;
|
|
365
|
+
_syncSource = "preset";
|
|
366
|
+
const expr = selectedPreset;
|
|
367
|
+
expressionToFields(expr);
|
|
368
|
+
rawValue = expr;
|
|
369
|
+
const valid = validateExpression(expr);
|
|
370
|
+
if (valid) {
|
|
371
|
+
value = expr;
|
|
372
|
+
}
|
|
373
|
+
onchange?.(expr, valid);
|
|
374
|
+
_syncSource = "";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// When only presets are visible, render as a plain select (no extra padding/border)
|
|
378
|
+
let presetsOnly = $derived(_showPresets && !_showFields && !_showRawInput && !_showDescription && !_showNextRun);
|
|
379
|
+
|
|
380
|
+
// Minute tick — triggers re-evaluation of "Next: ..." every 60s
|
|
381
|
+
let _tick = $state(0);
|
|
382
|
+
$effect(() => {
|
|
383
|
+
if (!_showNextRun) return;
|
|
384
|
+
const id = setInterval(() => _tick++, 60_000);
|
|
385
|
+
return () => clearInterval(id);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Human-readable description
|
|
389
|
+
let humanDescription = $derived.by(() => {
|
|
390
|
+
if (!_showDescription && !_showNextRun) return "";
|
|
391
|
+
const desc = _showDescription ? cronToHuman(rawValue) : "";
|
|
392
|
+
if (!_showNextRun || error) return desc;
|
|
393
|
+
// read _tick to create reactive dependency
|
|
394
|
+
void _tick;
|
|
395
|
+
try {
|
|
396
|
+
const parser = new CronParser(rawValue);
|
|
397
|
+
const next = parser.getNextRun();
|
|
398
|
+
const fmt = next.toLocaleString("sv-SE", {
|
|
399
|
+
year: "numeric",
|
|
400
|
+
month: "2-digit",
|
|
401
|
+
day: "2-digit",
|
|
402
|
+
hour: "2-digit",
|
|
403
|
+
minute: "2-digit",
|
|
404
|
+
});
|
|
405
|
+
return desc ? `${desc}. Next: ${fmt}` : `Next: ${fmt}`;
|
|
406
|
+
} catch {
|
|
407
|
+
return desc;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
</script>
|
|
411
|
+
|
|
412
|
+
<div bind:this={el}>
|
|
413
|
+
<InputWrap
|
|
414
|
+
{description}
|
|
415
|
+
class={classProp}
|
|
416
|
+
size={renderSize}
|
|
417
|
+
{id}
|
|
418
|
+
{label}
|
|
419
|
+
{labelAfter}
|
|
420
|
+
{below}
|
|
421
|
+
{required}
|
|
422
|
+
{disabled}
|
|
423
|
+
{labelLeft}
|
|
424
|
+
{labelLeftWidth}
|
|
425
|
+
{labelLeftBreakpoint}
|
|
426
|
+
{classLabel}
|
|
427
|
+
{classLabelBox}
|
|
428
|
+
{classInputBox}
|
|
429
|
+
{classInputBoxWrap}
|
|
430
|
+
classInputBoxWrapInvalid={twMerge(classInputBoxWrapInvalid)}
|
|
431
|
+
{classDescBox}
|
|
432
|
+
{classBelowBox}
|
|
433
|
+
{validation}
|
|
434
|
+
{style}
|
|
435
|
+
>
|
|
436
|
+
<div class="w-full flex">
|
|
437
|
+
<div
|
|
438
|
+
class={twMerge("stuic-cron-input-content", error && "has-error")}
|
|
439
|
+
data-presets-only={presetsOnly ? "" : undefined}
|
|
440
|
+
>
|
|
441
|
+
{#if _showPresets}
|
|
442
|
+
<select
|
|
443
|
+
class={twMerge("stuic-cron-input-preset", classPreset)}
|
|
444
|
+
bind:value={selectedPreset}
|
|
445
|
+
onchange={onPresetChange}
|
|
446
|
+
{disabled}
|
|
447
|
+
>
|
|
448
|
+
<option value="">Custom</option>
|
|
449
|
+
{#each _presets as p}
|
|
450
|
+
<option value={p.value}>{p.label}</option>
|
|
451
|
+
{/each}
|
|
452
|
+
</select>
|
|
453
|
+
{/if}
|
|
454
|
+
|
|
455
|
+
{#if _showFields}
|
|
456
|
+
<div class={twMerge("stuic-cron-input-fields", classFields)}>
|
|
457
|
+
{#each FIELD_DEFS as def}
|
|
458
|
+
<div class={twMerge("stuic-cron-input-field", classField)}>
|
|
459
|
+
<span class={twMerge("stuic-cron-input-field-label", classFieldLabel)}>
|
|
460
|
+
{def.label}
|
|
461
|
+
</span>
|
|
462
|
+
<input
|
|
463
|
+
type="text"
|
|
464
|
+
class={twMerge("stuic-cron-input-field-input", classFieldInput)}
|
|
465
|
+
bind:value={fields[def.key]}
|
|
466
|
+
oninput={onFieldInput}
|
|
467
|
+
placeholder={def.placeholder}
|
|
468
|
+
{disabled}
|
|
469
|
+
autocomplete="off"
|
|
470
|
+
spellcheck={false}
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
{/each}
|
|
474
|
+
</div>
|
|
475
|
+
{/if}
|
|
476
|
+
|
|
477
|
+
{#if _showRawInput}
|
|
478
|
+
<input
|
|
479
|
+
type="text"
|
|
480
|
+
class={twMerge("stuic-cron-input-raw", classRaw)}
|
|
481
|
+
bind:value={rawValue}
|
|
482
|
+
oninput={onRawInput}
|
|
483
|
+
placeholder="* * * * *"
|
|
484
|
+
{disabled}
|
|
485
|
+
autocomplete="off"
|
|
486
|
+
spellcheck={false}
|
|
487
|
+
/>
|
|
488
|
+
{/if}
|
|
489
|
+
|
|
490
|
+
{#if (_showDescription || _showNextRun) && humanDescription}
|
|
491
|
+
<div class={twMerge("stuic-cron-input-summary", classSummary)}>
|
|
492
|
+
{humanDescription}
|
|
493
|
+
</div>
|
|
494
|
+
{/if}
|
|
495
|
+
|
|
496
|
+
</div>
|
|
497
|
+
{#if hasModeToggle}
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
class={twMerge(BTN_CLS, classToggleButton)}
|
|
501
|
+
onclick={toggleMode}
|
|
502
|
+
{disabled}
|
|
503
|
+
use:tooltip={() => ({
|
|
504
|
+
enabled: true,
|
|
505
|
+
content: mode === "predefined" ? "Manual input" : "Predefined presets",
|
|
506
|
+
})}
|
|
507
|
+
>
|
|
508
|
+
{#if mode === "predefined"}
|
|
509
|
+
{@html iconSlidersHorizontal({ size: 19 })}
|
|
510
|
+
{:else}
|
|
511
|
+
{@html iconList({ size: 19 })}
|
|
512
|
+
{/if}
|
|
513
|
+
</button>
|
|
514
|
+
{/if}
|
|
515
|
+
</div>
|
|
516
|
+
</InputWrap>
|
|
517
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
3
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
4
|
+
type SnippetWithId = Snippet<[{
|
|
5
|
+
id: string;
|
|
6
|
+
}]>;
|
|
7
|
+
export interface CronPreset {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
export type CronInputMode = "predefined" | "manual";
|
|
12
|
+
export interface Props {
|
|
13
|
+
value?: string;
|
|
14
|
+
el?: HTMLElement;
|
|
15
|
+
id?: string;
|
|
16
|
+
mode?: CronInputMode;
|
|
17
|
+
label?: SnippetWithId | THC;
|
|
18
|
+
description?: SnippetWithId | THC;
|
|
19
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
23
|
+
class?: string;
|
|
24
|
+
style?: string;
|
|
25
|
+
labelLeft?: boolean;
|
|
26
|
+
labelLeftWidth?: "normal" | "wide";
|
|
27
|
+
labelLeftBreakpoint?: number;
|
|
28
|
+
labelAfter?: SnippetWithId | THC;
|
|
29
|
+
below?: SnippetWithId | THC;
|
|
30
|
+
showPresets?: boolean;
|
|
31
|
+
showFields?: boolean;
|
|
32
|
+
showRawInput?: boolean;
|
|
33
|
+
showDescription?: boolean;
|
|
34
|
+
showNextRun?: boolean;
|
|
35
|
+
presets?: CronPreset[];
|
|
36
|
+
onchange?: (expression: string, valid: boolean) => void;
|
|
37
|
+
classLabel?: string;
|
|
38
|
+
classLabelBox?: string;
|
|
39
|
+
classInputBox?: string;
|
|
40
|
+
classInputBoxWrap?: string;
|
|
41
|
+
classInputBoxWrapInvalid?: string;
|
|
42
|
+
classDescBox?: string;
|
|
43
|
+
classBelowBox?: string;
|
|
44
|
+
classFields?: string;
|
|
45
|
+
classField?: string;
|
|
46
|
+
classFieldLabel?: string;
|
|
47
|
+
classFieldInput?: string;
|
|
48
|
+
classPreset?: string;
|
|
49
|
+
classRaw?: string;
|
|
50
|
+
classSummary?: string;
|
|
51
|
+
classToggleButton?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare const DEFAULT_PRESETS: CronPreset[];
|
|
54
|
+
declare const CronInput: import("svelte").Component<Props, {}, "el" | "value" | "mode">;
|
|
55
|
+
type CronInput = ReturnType<typeof CronInput>;
|
|
56
|
+
export default CronInput;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reactive helper that parses a cron expression and computes the next run time,
|
|
3
|
+
* updating automatically every minute.
|
|
4
|
+
*/
|
|
5
|
+
export declare class CronNextRun {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(expression?: string);
|
|
8
|
+
get expression(): string;
|
|
9
|
+
set expression(v: string);
|
|
10
|
+
/** The next Date when the cron expression matches, or null if invalid. */
|
|
11
|
+
get nextRun(): Date | null;
|
|
12
|
+
/** Formatted next run string (YYYY-MM-DD HH:MM), or empty string if invalid. */
|
|
13
|
+
get nextRunFormatted(): string;
|
|
14
|
+
/** Whether the current expression is valid. */
|
|
15
|
+
get valid(): boolean;
|
|
16
|
+
/** Stop the internal timer. Call when no longer needed. */
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CronParser } from "@marianmeres/cron";
|
|
2
|
+
/**
|
|
3
|
+
* A reactive helper that parses a cron expression and computes the next run time,
|
|
4
|
+
* updating automatically every minute.
|
|
5
|
+
*/
|
|
6
|
+
export class CronNextRun {
|
|
7
|
+
#expression = $state("");
|
|
8
|
+
#tick = $state(0);
|
|
9
|
+
#interval;
|
|
10
|
+
constructor(expression = "* * * * *") {
|
|
11
|
+
this.#expression = expression;
|
|
12
|
+
this.#interval = setInterval(() => this.#tick++, 60_000);
|
|
13
|
+
}
|
|
14
|
+
get expression() {
|
|
15
|
+
return this.#expression;
|
|
16
|
+
}
|
|
17
|
+
set expression(v) {
|
|
18
|
+
this.#expression = v;
|
|
19
|
+
}
|
|
20
|
+
/** The next Date when the cron expression matches, or null if invalid. */
|
|
21
|
+
get nextRun() {
|
|
22
|
+
// read tick to create reactive dependency
|
|
23
|
+
void this.#tick;
|
|
24
|
+
try {
|
|
25
|
+
const parser = new CronParser(this.#expression);
|
|
26
|
+
return parser.getNextRun();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Formatted next run string (YYYY-MM-DD HH:MM), or empty string if invalid. */
|
|
33
|
+
get nextRunFormatted() {
|
|
34
|
+
const next = this.nextRun;
|
|
35
|
+
if (!next)
|
|
36
|
+
return "";
|
|
37
|
+
return next.toLocaleString("sv-SE", {
|
|
38
|
+
year: "numeric",
|
|
39
|
+
month: "2-digit",
|
|
40
|
+
day: "2-digit",
|
|
41
|
+
hour: "2-digit",
|
|
42
|
+
minute: "2-digit",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Whether the current expression is valid. */
|
|
46
|
+
get valid() {
|
|
47
|
+
try {
|
|
48
|
+
new CronParser(this.#expression);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Stop the internal timer. Call when no longer needed. */
|
|
56
|
+
destroy() {
|
|
57
|
+
if (this.#interval) {
|
|
58
|
+
clearInterval(this.#interval);
|
|
59
|
+
this.#interval = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|