@svar-ui/svelte-calendar 2.6.0 → 2.7.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/Calendar.svelte +13 -4
- package/dist/components/Calendar.svelte.d.ts +50 -19
- package/dist/components/ContextMenu.svelte +13 -5
- package/dist/components/Editor.svelte +29 -34
- package/dist/components/EventDatesForm.svelte +121 -0
- package/dist/components/EventDatesForm.svelte.d.ts +15 -0
- package/dist/components/Layout.svelte +18 -5
- package/dist/components/Layout.svelte.d.ts +2 -0
- package/dist/components/Navigation.svelte +20 -12
- package/dist/components/Navigation.svelte.d.ts +1 -0
- package/dist/components/Render/BarSection.svelte +3 -0
- package/dist/components/Render/EventProjection.svelte +36 -0
- package/dist/components/Render/EventProjection.svelte.d.ts +9 -0
- package/dist/components/Render/GridSection.svelte +5 -2
- package/dist/components/Render/GridSection.svelte.d.ts +2 -1
- package/dist/components/Render/ScrollableSection.svelte +22 -3
- package/dist/components/Render/ScrollableSection.svelte.d.ts +1 -0
- package/dist/components/Render/SectionContent.svelte +1 -0
- package/dist/components/Render/Sections.svelte +46 -9
- package/dist/components/Render/Sections.svelte.d.ts +1 -0
- package/dist/components/Render/YearSection.svelte +8 -1
- package/dist/components/Render/resolveEventPosition.d.ts +5 -0
- package/dist/components/Render/resolveEventPosition.js +35 -0
- package/dist/components/pro_RecurringForm.svelte +346 -0
- package/dist/components/pro_RecurringForm.svelte.d.ts +15 -0
- package/dist/components/pro_rrule-form.d.ts +23 -0
- package/dist/components/pro_rrule-form.js +121 -0
- package/dist/components/{Render/useEventOverlay.svelte.d.ts → useEventOverlay.svelte.d.ts} +1 -1
- package/dist/components/{Render/useEventOverlay.svelte.js → useEventOverlay.svelte.js} +1 -1
- package/dist/context.tx +0 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +15 -0
- package/dist/directives/clickevent.d.ts +1 -0
- package/dist/directives/clickevent.js +11 -8
- package/dist/directives/drag.js +11 -7
- package/dist/directives/dragresize.js +6 -3
- package/dist/helpers/pro_export.d.ts +5 -0
- package/dist/helpers/pro_export.js +22 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -2
- package/dist/pro_export/Calendar.svelte +30 -0
- package/dist/pro_export/Calendar.svelte.d.ts +16 -0
- package/dist/pro_export/Event.svelte +5 -0
- package/dist/pro_export/Event.svelte.d.ts +11 -0
- package/dist/pro_export.d.ts +3 -0
- package/dist/pro_export.js +15 -0
- package/dist/themes/Print.svelte +89 -0
- package/dist/themes/Print.svelte.d.ts +13 -0
- package/dist/themes/PrintBW.svelte +111 -0
- package/dist/themes/PrintBW.svelte.d.ts +13 -0
- package/dist/{components/types.d.ts → types.d.ts} +5 -0
- package/package.json +17 -17
- package/readme.md +2 -2
- package/dist/Demo.svelte +0 -7
- package/dist/Demo.svelte.d.ts +0 -26
- package/dist/components/editorItems.d.ts +0 -15
- package/dist/components/editorItems.js +0 -20
- package/dist/init.d.ts +0 -1
- package/dist/init.js +0 -5
- /package/dist/{components/types.js → types.js} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">const { primitives, dx, dy } = $props();
|
|
2
|
+
const gap = 2;
|
|
3
|
+
function style(primitive) {
|
|
4
|
+
const left = primitive.x * dx + gap;
|
|
5
|
+
const top = primitive.y * dy + gap;
|
|
6
|
+
const width = Math.max(2, primitive.width * dx - gap * 2);
|
|
7
|
+
const height = Math.max(2, primitive.height * dy - gap * 2);
|
|
8
|
+
return `left:${left}px;top:${top}px;width:${width}px;height:${height}px`;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="wx-event-placeholders" aria-hidden="true">
|
|
14
|
+
{#each primitives as primitive (primitive.id)}
|
|
15
|
+
<div class="wx-event-placeholder" style={style(primitive)}></div>
|
|
16
|
+
{/each}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.wx-event-placeholders {
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: 0;
|
|
23
|
+
z-index: 5;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.wx-event-placeholder {
|
|
28
|
+
position: absolute;
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
min-width: 2px;
|
|
31
|
+
min-height: 2px;
|
|
32
|
+
border: 2px solid var(--wx-color-primary);
|
|
33
|
+
border-radius: var(--wx-border-radius);
|
|
34
|
+
background: color-mix(in srgb, var(--wx-color-primary) 20%, var(--wx-background) 30%, transparent);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Primitive } from "@svar-ui/calendar-store";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
primitives: Primitive[];
|
|
4
|
+
dx: number;
|
|
5
|
+
dy: number;
|
|
6
|
+
};
|
|
7
|
+
declare const EventProjection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type EventProjection = ReturnType<typeof EventProjection>;
|
|
9
|
+
export default EventProjection;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import { setID } from "@svar-ui/lib-dom";
|
|
2
|
-
const { primitives, cells, dx, dy, cellCss, eventCss, eventContent, view, section, onoverflow } = $props();
|
|
2
|
+
const { primitives, cells, dx, dy, cellCss, eventCss, eventContent, view, section, eventOverflow = "more", onoverflow } = $props();
|
|
3
3
|
const laneHeight = 22;
|
|
4
4
|
const gap = 2;
|
|
5
5
|
const fullLaneHeight = laneHeight + gap * 2;
|
|
@@ -27,7 +27,7 @@ const rowLayout = $derived.by(() => {
|
|
|
27
27
|
// Only reserve space for "+more" label if lanes don't all fit
|
|
28
28
|
const allFit = Math.floor(groupHeight / fullLaneHeight) >= info.totalLanes;
|
|
29
29
|
const maxVisible = allFit ? info.totalLanes : Math.max(0, Math.floor((groupHeight - moreLabelHeight) / fullLaneHeight));
|
|
30
|
-
const isExpanded = expandedRows.has(y);
|
|
30
|
+
const isExpanded = eventOverflow === "expand" || expandedRows.has(y);
|
|
31
31
|
let extraHeight = 0;
|
|
32
32
|
if (isExpanded && info.totalLanes > maxVisible) {
|
|
33
33
|
// Extra pixels needed beyond the normal row height
|
|
@@ -298,6 +298,9 @@ function toggleRow(y) {
|
|
|
298
298
|
box-shadow: none;
|
|
299
299
|
border-radius: 0;
|
|
300
300
|
}
|
|
301
|
+
.wx-bar-single-day:global(.wx-dragging) {
|
|
302
|
+
background-color: var(--wx-background);
|
|
303
|
+
}
|
|
301
304
|
.wx-bar-single-day:hover {
|
|
302
305
|
background-color: var(--wx-color-secondary-hover);
|
|
303
306
|
box-shadow: none;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Primitive, GridCell, CellCss, EventCss } from "@svar-ui/calendar-store";
|
|
1
|
+
import type { Primitive, GridCell, CellCss, EventCss, EventOverflowMode } from "@svar-ui/calendar-store";
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
primitives: Primitive[];
|
|
4
4
|
cells: GridCell[];
|
|
@@ -9,6 +9,7 @@ type $$ComponentProps = {
|
|
|
9
9
|
eventContent?: any;
|
|
10
10
|
view: string;
|
|
11
11
|
section: string;
|
|
12
|
+
eventOverflow?: EventOverflowMode;
|
|
12
13
|
onoverflow?: (overflow: boolean) => void;
|
|
13
14
|
};
|
|
14
15
|
declare const GridSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
<script lang="ts">import { getContext, onMount } from "svelte";
|
|
1
|
+
<script lang="ts">import { getContext, onMount, untrack } from "svelte";
|
|
2
2
|
import { drag } from "../../directives/drag.js";
|
|
3
3
|
import { clickevent } from "../../directives/clickevent.js";
|
|
4
4
|
import { clickdate } from "../../directives/clickdate.js";
|
|
5
5
|
import { Popup } from "@svar-ui/svelte-core";
|
|
6
6
|
import Headers from "./Headers.svelte";
|
|
7
7
|
import SectionContent from "./SectionContent.svelte";
|
|
8
|
-
import
|
|
8
|
+
import EventProjection from "./EventProjection.svelte";
|
|
9
|
+
import { resolveEventPosition } from "./resolveEventPosition.js";
|
|
10
|
+
import { useEventOverlay } from "../useEventOverlay.svelte.js";
|
|
9
11
|
const api = getContext("calendar-api");
|
|
10
12
|
const { _view } = api.getReactiveState();
|
|
11
|
-
const { data, cellCss, eventCss, eventContent, view, tooltip, eventPopup, readonly = false } = $props();
|
|
13
|
+
const { data, cellCss, eventCss, eventContent, view, tooltip, eventPopup, readonly = false, eventProjection } = $props();
|
|
12
14
|
const section = $derived(data[0]);
|
|
13
15
|
const xHeaders = $derived(section?.xHeaders ?? null);
|
|
14
16
|
const yHeaders = $derived(section?.yHeaders ?? null);
|
|
@@ -55,6 +57,16 @@ function getMinContentHeight() {
|
|
|
55
57
|
}
|
|
56
58
|
const minW = $derived(getMinContentWidth());
|
|
57
59
|
const minH = $derived(getMinContentHeight());
|
|
60
|
+
const projection = $derived.by(() => {
|
|
61
|
+
if (!eventProjection || !eventProjection.htmlEvent) return null;
|
|
62
|
+
const event = resolveEventPosition(eventProjection.htmlEvent, eventProjection.event, section, contentEl, dx, dy, $_view, document);
|
|
63
|
+
if (!event) return null;
|
|
64
|
+
// store calculated props on the original projection object
|
|
65
|
+
untrack(() => {
|
|
66
|
+
Object.assign(eventProjection.event, event);
|
|
67
|
+
});
|
|
68
|
+
return $_view.projectEvent(event).find((item) => item.section === section.name);
|
|
69
|
+
});
|
|
58
70
|
const overlay = useEventOverlay((id) => api.getEvent(id), () => section?.mode === "boxes" ? "right-start" : "bottom-start");
|
|
59
71
|
// trackScroll exists in Popup but is missing from its .d.ts
|
|
60
72
|
const popupExtra = { trackScroll: true };
|
|
@@ -124,6 +136,13 @@ const popupExtra = { trackScroll: true };
|
|
|
124
136
|
{view}
|
|
125
137
|
{tooltip}
|
|
126
138
|
/>
|
|
139
|
+
{#if projection}
|
|
140
|
+
<EventProjection
|
|
141
|
+
primitives={projection.primitives}
|
|
142
|
+
{dx}
|
|
143
|
+
{dy}
|
|
144
|
+
/>
|
|
145
|
+
{/if}
|
|
127
146
|
{/if}
|
|
128
147
|
</div>
|
|
129
148
|
</div>
|
|
@@ -8,6 +8,7 @@ type $$ComponentProps = {
|
|
|
8
8
|
tooltip?: any;
|
|
9
9
|
eventPopup?: any;
|
|
10
10
|
readonly?: boolean;
|
|
11
|
+
eventProjection?: any;
|
|
11
12
|
};
|
|
12
13
|
declare const ScrollableSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
14
|
type ScrollableSection = ReturnType<typeof ScrollableSection>;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
<script lang="ts">import { getContext, onMount, tick } from "svelte";
|
|
1
|
+
<script lang="ts">import { getContext, onMount, tick, untrack } from "svelte";
|
|
2
2
|
import { drag } from "../../directives/drag.js";
|
|
3
3
|
import { clickevent } from "../../directives/clickevent.js";
|
|
4
4
|
import { clickdate } from "../../directives/clickdate.js";
|
|
5
5
|
import { Popup } from "@svar-ui/svelte-core";
|
|
6
6
|
import Headers from "./Headers.svelte";
|
|
7
7
|
import SectionContent from "./SectionContent.svelte";
|
|
8
|
-
import
|
|
8
|
+
import EventProjection from "./EventProjection.svelte";
|
|
9
|
+
import { resolveEventPosition } from "./resolveEventPosition.js";
|
|
10
|
+
import { useEventOverlay } from "../useEventOverlay.svelte.js";
|
|
9
11
|
const api = getContext("calendar-api");
|
|
10
12
|
const { _view } = api.getReactiveState();
|
|
11
|
-
const { data, cellCss, eventCss, eventContent, view, tooltip, eventPopup, readonly = false } = $props();
|
|
13
|
+
const { data, cellCss, eventCss, eventContent, view, tooltip, eventPopup, readonly = false, eventProjection } = $props();
|
|
12
14
|
let ready = $state(false);
|
|
13
15
|
let sectionEls = $state({});
|
|
14
16
|
let contentEls = $state({});
|
|
@@ -84,6 +86,9 @@ function getBarSectionHeight(section) {
|
|
|
84
86
|
const lanes = p.totalLanes ?? 1;
|
|
85
87
|
if (lanes > maxLanes) maxLanes = lanes;
|
|
86
88
|
}
|
|
89
|
+
if (!maxLanes && projectionFor(section.name)) {
|
|
90
|
+
maxLanes = 1;
|
|
91
|
+
}
|
|
87
92
|
return maxLanes * BAR_LANE_HEIGHT;
|
|
88
93
|
}
|
|
89
94
|
function dy(name, section) {
|
|
@@ -98,7 +103,24 @@ function sectionMinHeight(section) {
|
|
|
98
103
|
}
|
|
99
104
|
return getMinContentHeight(section);
|
|
100
105
|
}
|
|
101
|
-
const
|
|
106
|
+
const projections = $derived.by(() => {
|
|
107
|
+
if (!eventProjection || !eventProjection.htmlEvent) return [];
|
|
108
|
+
let event;
|
|
109
|
+
for (let item of visibleSections) {
|
|
110
|
+
event = resolveEventPosition(eventProjection.htmlEvent, eventProjection.event, item, contentEls[item.name], dx(item.name), dy(item.name, item), $_view, document);
|
|
111
|
+
if (event) break;
|
|
112
|
+
}
|
|
113
|
+
if (!event) return [];
|
|
114
|
+
// store calculated props on the original projection object
|
|
115
|
+
untrack(() => {
|
|
116
|
+
Object.assign(eventProjection.event, event);
|
|
117
|
+
});
|
|
118
|
+
return $_view.projectEvent(event);
|
|
119
|
+
});
|
|
120
|
+
function projectionFor(section) {
|
|
121
|
+
return projections.find((item) => item.section === section);
|
|
122
|
+
}
|
|
123
|
+
const visibleSections = $derived(data.filter((s) => s.size !== "content-optional" || s.primitives.length > 0 || !!projectionFor(s.name)));
|
|
102
124
|
// Sticky prefix: consecutive content-sized sections from the top.
|
|
103
125
|
// The first non-content section ends the prefix; later content sections
|
|
104
126
|
// (if any) scroll normally.
|
|
@@ -122,9 +144,13 @@ const stickyOffsets = $derived.by(() => {
|
|
|
122
144
|
}
|
|
123
145
|
return offsets;
|
|
124
146
|
});
|
|
125
|
-
let gridOverflow = $state(
|
|
126
|
-
function onGridOverflow(overflow) {
|
|
127
|
-
gridOverflow
|
|
147
|
+
let gridOverflow = $state({});
|
|
148
|
+
function onGridOverflow(section, overflow) {
|
|
149
|
+
if (!!gridOverflow[section] === overflow) return;
|
|
150
|
+
gridOverflow = {
|
|
151
|
+
...gridOverflow,
|
|
152
|
+
[section]: overflow
|
|
153
|
+
};
|
|
128
154
|
}
|
|
129
155
|
const hasYHeaders = $derived(visibleSections.some((s) => s.yHeaders !== null && s.yVisible !== false));
|
|
130
156
|
const xHeaders = $derived(visibleSections.find((s) => s.xHeaders !== null && s.xVisible !== false)?.xHeaders ?? null);
|
|
@@ -160,11 +186,13 @@ const popupExtra = { trackScroll: true };
|
|
|
160
186
|
{@const secDx = dx(section.name)}
|
|
161
187
|
{@const secDy = dy(section.name, section)}
|
|
162
188
|
{@const minH = sectionMinHeight(section)}
|
|
189
|
+
{@const projection = projectionFor(section.name)}
|
|
163
190
|
<div
|
|
164
191
|
class="wx-section"
|
|
165
192
|
class:wx-section-last={idx === visibleSections.length - 1}
|
|
166
193
|
class:wx-section-sticky={sticky}
|
|
167
|
-
class:wx-section-grid={section.mode === "grid" &&
|
|
194
|
+
class:wx-section-grid={section.mode === "grid" &&
|
|
195
|
+
!!gridOverflow[section.name]}
|
|
168
196
|
class:wx-has-y-headers={hasYHeaders}
|
|
169
197
|
style:flex={sectionFlex(section, sticky)}
|
|
170
198
|
style:min-height={minH > 0 ? `${minH}px` : undefined}
|
|
@@ -221,8 +249,17 @@ const popupExtra = { trackScroll: true };
|
|
|
221
249
|
{eventContent}
|
|
222
250
|
{view}
|
|
223
251
|
{tooltip}
|
|
224
|
-
onoverflow={section.mode === "grid"
|
|
252
|
+
onoverflow={section.mode === "grid"
|
|
253
|
+
? overflow => onGridOverflow(section.name, overflow)
|
|
254
|
+
: undefined}
|
|
225
255
|
/>
|
|
256
|
+
{#if projection}
|
|
257
|
+
<EventProjection
|
|
258
|
+
primitives={projection.primitives}
|
|
259
|
+
dx={secDx}
|
|
260
|
+
dy={secDy}
|
|
261
|
+
/>
|
|
262
|
+
{/if}
|
|
226
263
|
</div>
|
|
227
264
|
</div>
|
|
228
265
|
{/each}
|
|
@@ -85,7 +85,7 @@ function eventTitle(event) {
|
|
|
85
85
|
|
|
86
86
|
<div
|
|
87
87
|
class="wx-year-grid"
|
|
88
|
-
style="
|
|
88
|
+
style="--wx-year-columns: {columns}"
|
|
89
89
|
>
|
|
90
90
|
{#each months as month (month.month)}
|
|
91
91
|
<div class="wx-year-month">
|
|
@@ -163,10 +163,17 @@ function eventTitle(event) {
|
|
|
163
163
|
<style>
|
|
164
164
|
.wx-year-grid {
|
|
165
165
|
display: grid;
|
|
166
|
+
grid-template-columns: repeat(var(--wx-year-columns, 3), 1fr);
|
|
166
167
|
gap: 16px;
|
|
167
168
|
padding: 16px;
|
|
168
169
|
position: relative;
|
|
169
170
|
}
|
|
171
|
+
:global(.wx-calendar--compact) {
|
|
172
|
+
.wx-year-grid {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
170
177
|
.wx-month-label {
|
|
171
178
|
font-weight: var(--wx-font-weight-md);
|
|
172
179
|
margin-bottom: 4px;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CalendarEvent, SectionResult, ViewModel } from "@svar-ui/calendar-store";
|
|
2
|
+
import type { CalendarPoint } from "../../types.js";
|
|
3
|
+
export declare function resolveEventPosition(point: CalendarPoint, event: Partial<CalendarEvent>, section: SectionResult, element: HTMLElement, dx: number, dy: number, model: ViewModel, doc: {
|
|
4
|
+
elementFromPoint: (x: number, y: number) => Element | null;
|
|
5
|
+
}): Partial<CalendarEvent> | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function resolveEventPosition(point, event, section, element, dx, dy, model, doc) {
|
|
2
|
+
const hit = doc.elementFromPoint(point.clientX, point.clientY);
|
|
3
|
+
if (!element.contains(hit))
|
|
4
|
+
return null;
|
|
5
|
+
if (section.mode === "list" ||
|
|
6
|
+
section.mode === "year" ||
|
|
7
|
+
dx <= 0 ||
|
|
8
|
+
dy <= 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const rect = element.getBoundingClientRect();
|
|
12
|
+
if (point.clientX < rect.left ||
|
|
13
|
+
point.clientX > rect.right ||
|
|
14
|
+
point.clientY < rect.top ||
|
|
15
|
+
point.clientY > rect.bottom) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const x = Math.max(0, Math.min(99.999, (point.clientX - rect.left + element.scrollLeft) / dx));
|
|
19
|
+
const y = Math.max(0, Math.min(99.999, (point.clientY - rect.top + element.scrollTop) / dy));
|
|
20
|
+
const duration = typeof event.duration === "number" && event.duration > 0
|
|
21
|
+
? event.duration
|
|
22
|
+
: event.start instanceof Date && event.end instanceof Date
|
|
23
|
+
? event.end.getTime() - event.start.getTime()
|
|
24
|
+
: 0;
|
|
25
|
+
const positioned = model.toPositionStart(section.name, x, y, event, true);
|
|
26
|
+
if (!(positioned.start instanceof Date))
|
|
27
|
+
return null;
|
|
28
|
+
if (duration > 0) {
|
|
29
|
+
positioned.end = new Date(positioned.start.getTime() + duration);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
delete positioned.end;
|
|
33
|
+
}
|
|
34
|
+
return positioned;
|
|
35
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, setContext } from "svelte";
|
|
3
|
+
import {
|
|
4
|
+
DatePicker,
|
|
5
|
+
TimePicker,
|
|
6
|
+
Select,
|
|
7
|
+
Checkbox,
|
|
8
|
+
Counter,
|
|
9
|
+
} from "@svar-ui/svelte-core";
|
|
10
|
+
import {
|
|
11
|
+
parseRRule,
|
|
12
|
+
buildRRule,
|
|
13
|
+
WEEKDAYS,
|
|
14
|
+
weekdayOf,
|
|
15
|
+
weekdayName,
|
|
16
|
+
nthOf,
|
|
17
|
+
nthLabel,
|
|
18
|
+
} from "./pro_rrule-form";
|
|
19
|
+
|
|
20
|
+
const locale = getContext("wx-i18n");
|
|
21
|
+
const _ = locale ? locale.getGroup("eventCalendar") : v => v;
|
|
22
|
+
|
|
23
|
+
setContext("wx-input-id", "");
|
|
24
|
+
|
|
25
|
+
let { value, error, onchange } = $props();
|
|
26
|
+
|
|
27
|
+
const update = part => onchange({ value: { ...value, ...part } });
|
|
28
|
+
|
|
29
|
+
function sameDay(a, b) {
|
|
30
|
+
return (
|
|
31
|
+
a.getFullYear() === b.getFullYear() &&
|
|
32
|
+
a.getMonth() === b.getMonth() &&
|
|
33
|
+
a.getDate() === b.getDate()
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// calendar returns the picked day at midnight; keep the current time part
|
|
38
|
+
function pickDate(key, day) {
|
|
39
|
+
if (!day) return;
|
|
40
|
+
const current = value[key];
|
|
41
|
+
const next = new Date(day);
|
|
42
|
+
if (current) {
|
|
43
|
+
next.setHours(current.getHours(), current.getMinutes(), 0, 0);
|
|
44
|
+
}
|
|
45
|
+
if (key === "start" && sameDay(value.start, value.end)) {
|
|
46
|
+
const end = new Date(value.end);
|
|
47
|
+
end.setFullYear(next.getFullYear(), next.getMonth(), next.getDate());
|
|
48
|
+
update({ start: next, end });
|
|
49
|
+
} else {
|
|
50
|
+
update({ [key]: next });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// time picker returns hours/minutes only; keep the current date part
|
|
55
|
+
function pickTime(key, time) {
|
|
56
|
+
const current = value[key];
|
|
57
|
+
if (!current) return;
|
|
58
|
+
const next = new Date(current);
|
|
59
|
+
next.setHours(time.getHours(), time.getMinutes(), 0, 0);
|
|
60
|
+
update({ [key]: next });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// local editable model; buildRRule(form) may collapse a custom setup
|
|
64
|
+
// into an equivalent preset string (custom / every 1 day -> FREQ=DAILY),
|
|
65
|
+
// so re-seeding from the incoming rrule is skipped while it equals the
|
|
66
|
+
// last string this component emitted - otherwise the autoSave echo
|
|
67
|
+
// would flip the "Custom" select back to the preset
|
|
68
|
+
// svelte-ignore state_referenced_locally
|
|
69
|
+
let form = $state(parseRRule(value?.rrule, value?.start));
|
|
70
|
+
// svelte-ignore state_referenced_locally
|
|
71
|
+
let lastRRule = value?.rrule ?? "";
|
|
72
|
+
|
|
73
|
+
$effect.pre(() => {
|
|
74
|
+
const incoming = value?.rrule ?? "";
|
|
75
|
+
if (incoming !== lastRRule) {
|
|
76
|
+
lastRRule = incoming;
|
|
77
|
+
form = parseRRule(incoming, value?.start);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function applyForm(part) {
|
|
82
|
+
Object.assign(form, part);
|
|
83
|
+
const rrule = buildRRule(form, value.start);
|
|
84
|
+
lastRRule = rrule;
|
|
85
|
+
update({ rrule });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setType(type) {
|
|
89
|
+
const part = { type };
|
|
90
|
+
// pre-seed the custom block so it opens in a sensible state
|
|
91
|
+
if (type === "custom" && !form.weekdays.length)
|
|
92
|
+
part.weekdays = [weekdayOf(value.start)];
|
|
93
|
+
applyForm(part);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setEnds(ends) {
|
|
97
|
+
const part = { ends };
|
|
98
|
+
if (ends === "until" && !form.until) {
|
|
99
|
+
const d = new Date(value.start);
|
|
100
|
+
d.setMonth(d.getMonth() + 1);
|
|
101
|
+
part.until = d;
|
|
102
|
+
}
|
|
103
|
+
applyForm(part);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toggleDay(id, checked) {
|
|
107
|
+
// unchecking all days is allowed - BYDAY is then omitted and the
|
|
108
|
+
// rule falls back to the start date's weekday
|
|
109
|
+
applyForm({
|
|
110
|
+
weekdays: checked
|
|
111
|
+
? [...form.weekdays, id]
|
|
112
|
+
: form.weekdays.filter(d => d !== id),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const repeatOptions = [
|
|
117
|
+
{ id: "never", label: _("Never") },
|
|
118
|
+
{ id: "daily", label: _("Daily") },
|
|
119
|
+
{ id: "weekly", label: _("Weekly") },
|
|
120
|
+
{ id: "monthly", label: _("Monthly") },
|
|
121
|
+
{ id: "yearly", label: _("Yearly") },
|
|
122
|
+
{ id: "workdays", label: _("Every workday") },
|
|
123
|
+
{ id: "custom", label: _("Custom") },
|
|
124
|
+
];
|
|
125
|
+
const unitOptions = [
|
|
126
|
+
{ id: "DAILY", label: _("day(s)") },
|
|
127
|
+
{ id: "WEEKLY", label: _("week(s)") },
|
|
128
|
+
{ id: "MONTHLY", label: _("month(s)") },
|
|
129
|
+
{ id: "YEARLY", label: _("year(s)") },
|
|
130
|
+
];
|
|
131
|
+
const endsOptions = [
|
|
132
|
+
{ id: "never", label: _("Never") },
|
|
133
|
+
{ id: "count", label: _("After") },
|
|
134
|
+
{ id: "until", label: _("On date") },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
function formatLabel(template, values) {
|
|
138
|
+
return Object.entries(values).reduce(
|
|
139
|
+
(label, [key, value]) => label.replace(`%${key}`, String(value)),
|
|
140
|
+
template
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// labels follow the current start date; a stored rule is re-derived
|
|
145
|
+
// from the new start only on the next recurrence edit
|
|
146
|
+
const monthModeOptions = $derived([
|
|
147
|
+
{
|
|
148
|
+
id: "day",
|
|
149
|
+
label: formatLabel(_("on day %d"), {
|
|
150
|
+
d: value?.start?.getDate() ?? 1,
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "weekday",
|
|
155
|
+
label: formatLabel(_("on the %n %d"), {
|
|
156
|
+
n: _(nthLabel(nthOf(value?.start))),
|
|
157
|
+
d: _(weekdayName(weekdayOf(value?.start))),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<div class="wx-recurrence" class:wx-error={!!error}>
|
|
164
|
+
<div class="wx-row">
|
|
165
|
+
<span class="wx-date-label">{_("Start date")}</span>
|
|
166
|
+
<div class="wx-date">
|
|
167
|
+
<DatePicker
|
|
168
|
+
value={value.start}
|
|
169
|
+
buttons={false}
|
|
170
|
+
onchange={ev => pickDate("start", ev.value)}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
{#if !value.allDay}
|
|
174
|
+
<div class="wx-time">
|
|
175
|
+
<TimePicker
|
|
176
|
+
value={value.start}
|
|
177
|
+
onchange={ev => pickTime("start", ev.value)}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="wx-row">
|
|
184
|
+
<span class="wx-date-label">{_("End date")}</span>
|
|
185
|
+
<div class="wx-date">
|
|
186
|
+
<DatePicker
|
|
187
|
+
value={value.end}
|
|
188
|
+
buttons={false}
|
|
189
|
+
onchange={ev => pickDate("end", ev.value)}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
{#if !value.allDay}
|
|
193
|
+
<div class="wx-time">
|
|
194
|
+
<TimePicker
|
|
195
|
+
value={value.end}
|
|
196
|
+
onchange={ev => pickTime("end", ev.value)}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
{/if}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="wx-row">
|
|
203
|
+
<span class="wx-date-label"></span>
|
|
204
|
+
<Checkbox
|
|
205
|
+
label={_("All day")}
|
|
206
|
+
value={!!value.allDay}
|
|
207
|
+
onchange={ev => update({ allDay: ev.value })}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="wx-row">
|
|
212
|
+
<span class="wx-label">{_("Repeat")}</span>
|
|
213
|
+
<div class="wx-control">
|
|
214
|
+
<Select
|
|
215
|
+
value={form.type}
|
|
216
|
+
options={repeatOptions}
|
|
217
|
+
onchange={ev => setType(ev.value)}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{#if form.type === "custom"}
|
|
223
|
+
<div class="wx-row">
|
|
224
|
+
<span class="wx-label">{_("Every")}</span>
|
|
225
|
+
<div class="wx-counter">
|
|
226
|
+
<Counter
|
|
227
|
+
value={form.interval}
|
|
228
|
+
min={1}
|
|
229
|
+
max={999}
|
|
230
|
+
onchange={ev => !ev.input && applyForm({ interval: ev.value })}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="wx-control">
|
|
234
|
+
<Select
|
|
235
|
+
value={form.freq}
|
|
236
|
+
options={unitOptions}
|
|
237
|
+
onchange={ev => applyForm({ freq: ev.value })}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
{#if form.freq === "WEEKLY"}
|
|
242
|
+
<div class="wx-row">
|
|
243
|
+
<span class="wx-label"></span>
|
|
244
|
+
<div class="wx-days">
|
|
245
|
+
{#each WEEKDAYS as day (day.id)}
|
|
246
|
+
<Checkbox
|
|
247
|
+
label={_(day.label)}
|
|
248
|
+
value={form.weekdays.includes(day.id)}
|
|
249
|
+
onchange={ev => toggleDay(day.id, ev.value)}
|
|
250
|
+
/>
|
|
251
|
+
{/each}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
{:else if form.freq === "MONTHLY" || form.freq === "YEARLY"}
|
|
255
|
+
<div class="wx-row">
|
|
256
|
+
<span class="wx-label"></span>
|
|
257
|
+
<div class="wx-control">
|
|
258
|
+
<Select
|
|
259
|
+
value={form.monthMode}
|
|
260
|
+
options={monthModeOptions}
|
|
261
|
+
onchange={ev => applyForm({ monthMode: ev.value })}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
{/if}
|
|
267
|
+
|
|
268
|
+
{#if form.type !== "never"}
|
|
269
|
+
<div class="wx-row">
|
|
270
|
+
<span class="wx-label">{_("Ends")}</span>
|
|
271
|
+
<div class="wx-control">
|
|
272
|
+
<Select
|
|
273
|
+
value={form.ends}
|
|
274
|
+
options={endsOptions}
|
|
275
|
+
onchange={ev => setEnds(ev.value)}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
{#if form.ends === "count"}
|
|
279
|
+
<div class="wx-counter">
|
|
280
|
+
<Counter
|
|
281
|
+
value={form.count}
|
|
282
|
+
min={1}
|
|
283
|
+
max={999}
|
|
284
|
+
onchange={ev => !ev.input && applyForm({ count: ev.value })}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
<span class="wx-hint">{_("occurrence(s)")}</span>
|
|
288
|
+
{:else if form.ends === "until"}
|
|
289
|
+
<div class="wx-date">
|
|
290
|
+
<DatePicker
|
|
291
|
+
value={form.until}
|
|
292
|
+
onchange={ev => ev.value && applyForm({ until: ev.value })}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
{/if}
|
|
296
|
+
</div>
|
|
297
|
+
{/if}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<style>
|
|
301
|
+
.wx-recurrence {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
gap: var(--wx-padding);
|
|
305
|
+
}
|
|
306
|
+
.wx-row {
|
|
307
|
+
display: flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
gap: var(--wx-padding);
|
|
310
|
+
}
|
|
311
|
+
.wx-date-label,
|
|
312
|
+
.wx-label {
|
|
313
|
+
flex: none;
|
|
314
|
+
width: 52px;
|
|
315
|
+
}
|
|
316
|
+
.wx-date-label {
|
|
317
|
+
width: 72px;
|
|
318
|
+
}
|
|
319
|
+
.wx-control {
|
|
320
|
+
flex: 1;
|
|
321
|
+
}
|
|
322
|
+
.wx-date {
|
|
323
|
+
flex: 1;
|
|
324
|
+
min-width: 0;
|
|
325
|
+
}
|
|
326
|
+
.wx-time {
|
|
327
|
+
flex: none;
|
|
328
|
+
width: 96px;
|
|
329
|
+
}
|
|
330
|
+
.wx-counter {
|
|
331
|
+
flex: none;
|
|
332
|
+
width: 140px;
|
|
333
|
+
}
|
|
334
|
+
.wx-days {
|
|
335
|
+
flex: 1;
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-wrap: wrap;
|
|
338
|
+
gap: 4px 12px;
|
|
339
|
+
}
|
|
340
|
+
.wx-hint {
|
|
341
|
+
white-space: nowrap;
|
|
342
|
+
}
|
|
343
|
+
.wx-error :global(.wx-input) {
|
|
344
|
+
border-color: var(--wx-color-danger);
|
|
345
|
+
}
|
|
346
|
+
</style>
|