@svar-ui/svelte-calendar 2.6.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/Demo.svelte +7 -0
- package/dist/Demo.svelte.d.ts +26 -0
- package/dist/components/AddEventButton.svelte +12 -0
- package/dist/components/AddEventButton.svelte.d.ts +18 -0
- package/dist/components/Calendar.svelte +110 -0
- package/dist/components/Calendar.svelte.d.ts +153 -0
- package/dist/components/CalendarPanel.svelte +147 -0
- package/dist/components/CalendarPanel.svelte.d.ts +18 -0
- package/dist/components/ContextMenu.svelte +87 -0
- package/dist/components/ContextMenu.svelte.d.ts +17 -0
- package/dist/components/DateLabel.svelte +13 -0
- package/dist/components/DateLabel.svelte.d.ts +18 -0
- package/dist/components/DateNav.svelte +31 -0
- package/dist/components/DateNav.svelte.d.ts +18 -0
- package/dist/components/DateTimePicker.svelte +30 -0
- package/dist/components/DateTimePicker.svelte.d.ts +8 -0
- package/dist/components/Editor.svelte +123 -0
- package/dist/components/Editor.svelte.d.ts +10 -0
- package/dist/components/Layout.svelte +76 -0
- package/dist/components/Layout.svelte.d.ts +21 -0
- package/dist/components/MenuButton.svelte +9 -0
- package/dist/components/MenuButton.svelte.d.ts +18 -0
- package/dist/components/Navigation.svelte +59 -0
- package/dist/components/Navigation.svelte.d.ts +12 -0
- package/dist/components/Render/BarSection.svelte +89 -0
- package/dist/components/Render/BarSection.svelte.d.ts +13 -0
- package/dist/components/Render/BoxSection.svelte +94 -0
- package/dist/components/Render/BoxSection.svelte.d.ts +15 -0
- package/dist/components/Render/Grid.svelte +16 -0
- package/dist/components/Render/Grid.svelte.d.ts +14 -0
- package/dist/components/Render/GridCells.svelte +98 -0
- package/dist/components/Render/GridCells.svelte.d.ts +14 -0
- package/dist/components/Render/GridLines.svelte +45 -0
- package/dist/components/Render/GridLines.svelte.d.ts +10 -0
- package/dist/components/Render/GridSection.svelte +348 -0
- package/dist/components/Render/GridSection.svelte.d.ts +16 -0
- package/dist/components/Render/Headers.svelte +86 -0
- package/dist/components/Render/Headers.svelte.d.ts +8 -0
- package/dist/components/Render/ListSection.svelte +110 -0
- package/dist/components/Render/ListSection.svelte.d.ts +8 -0
- package/dist/components/Render/NowLine.svelte +68 -0
- package/dist/components/Render/NowLine.svelte.d.ts +8 -0
- package/dist/components/Render/ScrollableSection.svelte +207 -0
- package/dist/components/Render/ScrollableSection.svelte.d.ts +14 -0
- package/dist/components/Render/SectionContent.svelte +83 -0
- package/dist/components/Render/SectionContent.svelte.d.ts +17 -0
- package/dist/components/Render/Sections.svelte +340 -0
- package/dist/components/Render/Sections.svelte.d.ts +14 -0
- package/dist/components/Render/YearSection.svelte +265 -0
- package/dist/components/Render/YearSection.svelte.d.ts +9 -0
- package/dist/components/Render/useEventOverlay.svelte.d.ts +21 -0
- package/dist/components/Render/useEventOverlay.svelte.js +60 -0
- package/dist/components/TodayButton.svelte +11 -0
- package/dist/components/TodayButton.svelte.d.ts +18 -0
- package/dist/components/editorItems.d.ts +15 -0
- package/dist/components/editorItems.js +20 -0
- package/dist/components/types.d.ts +29 -0
- package/dist/components/types.js +1 -0
- package/dist/directives/clickdate.d.ts +8 -0
- package/dist/directives/clickdate.js +45 -0
- package/dist/directives/clickevent.d.ts +15 -0
- package/dist/directives/clickevent.js +54 -0
- package/dist/directives/drag.d.ts +19 -0
- package/dist/directives/drag.js +494 -0
- package/dist/directives/dragresize.d.ts +10 -0
- package/dist/directives/dragresize.js +58 -0
- package/dist/directives/resize.d.ts +5 -0
- package/dist/directives/resize.js +11 -0
- package/dist/dom.d.ts +1 -0
- package/dist/dom.js +12 -0
- package/dist/env.d.ts +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +5 -0
- package/dist/themes/Willow.svelte +16 -0
- package/dist/themes/Willow.svelte.d.ts +7 -0
- package/dist/themes/WillowDark.svelte +16 -0
- package/dist/themes/WillowDark.svelte.d.ts +7 -0
- package/license.txt +21 -0
- package/package.json +60 -0
- package/readme.md +107 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<script lang="ts">import { getContext, onMount, tick } from "svelte";
|
|
2
|
+
import { drag } from "../../directives/drag.js";
|
|
3
|
+
import { clickevent } from "../../directives/clickevent.js";
|
|
4
|
+
import { clickdate } from "../../directives/clickdate.js";
|
|
5
|
+
import { Popup } from "@svar-ui/svelte-core";
|
|
6
|
+
import Headers from "./Headers.svelte";
|
|
7
|
+
import SectionContent from "./SectionContent.svelte";
|
|
8
|
+
import { useEventOverlay } from "./useEventOverlay.svelte.js";
|
|
9
|
+
const api = getContext("calendar-api");
|
|
10
|
+
const { _view } = api.getReactiveState();
|
|
11
|
+
const { data, cellCss, eventCss, eventContent, view, tooltip, eventPopup, readonly = false } = $props();
|
|
12
|
+
let ready = $state(false);
|
|
13
|
+
let sectionEls = $state({});
|
|
14
|
+
let contentEls = $state({});
|
|
15
|
+
let sizes = $state({});
|
|
16
|
+
let xHeadersEl = $state();
|
|
17
|
+
let xHeadersHeight = $state(0);
|
|
18
|
+
function measure() {
|
|
19
|
+
const next = {};
|
|
20
|
+
let changed = false;
|
|
21
|
+
for (const section of data) {
|
|
22
|
+
const sectionEl = sectionEls[section.name];
|
|
23
|
+
const contentEl = contentEls[section.name];
|
|
24
|
+
if (sectionEl && contentEl) {
|
|
25
|
+
const w = contentEl.clientWidth;
|
|
26
|
+
const h = sectionEl.clientHeight;
|
|
27
|
+
next[section.name] = {
|
|
28
|
+
width: w,
|
|
29
|
+
height: h
|
|
30
|
+
};
|
|
31
|
+
const prev = sizes[section.name];
|
|
32
|
+
if (!prev || prev.width !== w || prev.height !== h) {
|
|
33
|
+
changed = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (changed) sizes = next;
|
|
38
|
+
const xh = xHeadersEl ? xHeadersEl.clientHeight : 0;
|
|
39
|
+
if (xh !== xHeadersHeight) xHeadersHeight = xh;
|
|
40
|
+
}
|
|
41
|
+
let ro = null;
|
|
42
|
+
function observeAll() {
|
|
43
|
+
if (!ro) return;
|
|
44
|
+
ro.disconnect();
|
|
45
|
+
for (const section of data) {
|
|
46
|
+
const el = sectionEls[section.name];
|
|
47
|
+
if (el) ro.observe(el);
|
|
48
|
+
}
|
|
49
|
+
if (xHeadersEl) ro.observe(xHeadersEl);
|
|
50
|
+
}
|
|
51
|
+
onMount(() => {
|
|
52
|
+
measure();
|
|
53
|
+
ready = true;
|
|
54
|
+
ro = new ResizeObserver(() => measure());
|
|
55
|
+
observeAll();
|
|
56
|
+
return () => ro?.disconnect();
|
|
57
|
+
});
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (!ready || !ro) return;
|
|
60
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
61
|
+
data;
|
|
62
|
+
tick().then(() => {
|
|
63
|
+
observeAll();
|
|
64
|
+
measure();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
function dx(name) {
|
|
68
|
+
return (sizes[name]?.width ?? 0) / 100;
|
|
69
|
+
}
|
|
70
|
+
function getMinContentHeight(section) {
|
|
71
|
+
if (!section.yHeaders) return 0;
|
|
72
|
+
const innerLevel = section.yHeaders[section.yHeaders.length - 1];
|
|
73
|
+
if (!innerLevel || innerLevel.length === 0) return 0;
|
|
74
|
+
const first = innerLevel[0];
|
|
75
|
+
const minUnitHeight = first.ui?.minUnitHeight;
|
|
76
|
+
return typeof minUnitHeight === "number" ? innerLevel.length * minUnitHeight : 0;
|
|
77
|
+
}
|
|
78
|
+
// Lane height must match BarSection.svelte
|
|
79
|
+
const BAR_LANE_HEIGHT = 28;
|
|
80
|
+
function getBarSectionHeight(section) {
|
|
81
|
+
if (section.mode !== "bars" || typeof section.size === "number") return 0;
|
|
82
|
+
let maxLanes = 0;
|
|
83
|
+
for (const p of section.primitives) {
|
|
84
|
+
const lanes = p.totalLanes ?? 1;
|
|
85
|
+
if (lanes > maxLanes) maxLanes = lanes;
|
|
86
|
+
}
|
|
87
|
+
return maxLanes * BAR_LANE_HEIGHT;
|
|
88
|
+
}
|
|
89
|
+
function dy(name, section) {
|
|
90
|
+
const containerHeight = sizes[name]?.height ?? 0;
|
|
91
|
+
const minHeight = getMinContentHeight(section);
|
|
92
|
+
const barHeight = getBarSectionHeight(section);
|
|
93
|
+
return Math.max(containerHeight, minHeight, barHeight) / 100;
|
|
94
|
+
}
|
|
95
|
+
function sectionMinHeight(section) {
|
|
96
|
+
if (typeof section.size !== "number") {
|
|
97
|
+
return getBarSectionHeight(section);
|
|
98
|
+
}
|
|
99
|
+
return getMinContentHeight(section);
|
|
100
|
+
}
|
|
101
|
+
const visibleSections = $derived(data.filter((s) => s.size !== "content-optional" || s.primitives.length > 0));
|
|
102
|
+
// Sticky prefix: consecutive content-sized sections from the top.
|
|
103
|
+
// The first non-content section ends the prefix; later content sections
|
|
104
|
+
// (if any) scroll normally.
|
|
105
|
+
const stickyCount = $derived.by(() => {
|
|
106
|
+
let count = 0;
|
|
107
|
+
for (const s of visibleSections) {
|
|
108
|
+
const sz = s.size ?? 1;
|
|
109
|
+
if (sz === "content" || sz === "content-optional") count++;
|
|
110
|
+
else break;
|
|
111
|
+
}
|
|
112
|
+
return count;
|
|
113
|
+
});
|
|
114
|
+
// Cumulative `top` for each sticky section, starting after x-headers.
|
|
115
|
+
const stickyOffsets = $derived.by(() => {
|
|
116
|
+
const offsets = [];
|
|
117
|
+
let acc = xHeadersHeight;
|
|
118
|
+
for (let i = 0; i < stickyCount; i++) {
|
|
119
|
+
offsets[i] = acc;
|
|
120
|
+
const name = visibleSections[i].name;
|
|
121
|
+
acc += sizes[name]?.height ?? 0;
|
|
122
|
+
}
|
|
123
|
+
return offsets;
|
|
124
|
+
});
|
|
125
|
+
let gridOverflow = $state(false);
|
|
126
|
+
function onGridOverflow(overflow) {
|
|
127
|
+
gridOverflow = overflow;
|
|
128
|
+
}
|
|
129
|
+
const hasYHeaders = $derived(visibleSections.some((s) => s.yHeaders !== null && s.yVisible !== false));
|
|
130
|
+
const xHeaders = $derived(visibleSections.find((s) => s.xHeaders !== null && s.xVisible !== false)?.xHeaders ?? null);
|
|
131
|
+
function sectionFlex(section, sticky) {
|
|
132
|
+
if (sticky) return "0 0 auto";
|
|
133
|
+
// list and year render as natural DOM flow — size to content,
|
|
134
|
+
// let the outer container scroll when total exceeds viewport.
|
|
135
|
+
if (section.mode === "list" || section.mode === "year") return "0 0 auto";
|
|
136
|
+
return typeof section.size === "number" ? "1" : "0 0 auto";
|
|
137
|
+
}
|
|
138
|
+
const overlay = useEventOverlay((id) => api.getEvent(id), (el) => {
|
|
139
|
+
const section = visibleSections.find((s) => sectionEls[s.name]?.contains(el));
|
|
140
|
+
return section?.mode === "boxes" ? "right-start" : "bottom-start";
|
|
141
|
+
});
|
|
142
|
+
// trackScroll exists in Popup but is missing from its .d.ts
|
|
143
|
+
const popupExtra = { trackScroll: true };
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<div class="wx-sections">
|
|
147
|
+
{#if xHeaders}
|
|
148
|
+
<div class="wx-x-headers-row" bind:this={xHeadersEl}>
|
|
149
|
+
{#if hasYHeaders}
|
|
150
|
+
<div class="wx-header-corner"></div>
|
|
151
|
+
{/if}
|
|
152
|
+
<div class="wx-x-headers-area">
|
|
153
|
+
<Headers headers={xHeaders} direction="x" />
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
{#each visibleSections as section, idx (section.name)}
|
|
159
|
+
{@const sticky = idx < stickyCount}
|
|
160
|
+
{@const secDx = dx(section.name)}
|
|
161
|
+
{@const secDy = dy(section.name, section)}
|
|
162
|
+
{@const minH = sectionMinHeight(section)}
|
|
163
|
+
<div
|
|
164
|
+
class="wx-section"
|
|
165
|
+
class:wx-section-last={idx === visibleSections.length - 1}
|
|
166
|
+
class:wx-section-sticky={sticky}
|
|
167
|
+
class:wx-section-grid={section.mode === "grid" && gridOverflow}
|
|
168
|
+
class:wx-has-y-headers={hasYHeaders}
|
|
169
|
+
style:flex={sectionFlex(section, sticky)}
|
|
170
|
+
style:min-height={minH > 0 ? `${minH}px` : undefined}
|
|
171
|
+
style:top={sticky ? `${stickyOffsets[idx] ?? 0}px` : undefined}
|
|
172
|
+
style:z-index={sticky ? 10 - idx : undefined}
|
|
173
|
+
bind:this={sectionEls[section.name]}
|
|
174
|
+
>
|
|
175
|
+
{#if section.yVisible !== false}
|
|
176
|
+
<div class="wx-y-headers-area">
|
|
177
|
+
<Headers headers={section.yHeaders} direction="y" />
|
|
178
|
+
</div>
|
|
179
|
+
{:else if hasYHeaders}
|
|
180
|
+
<div class="wx-header-spacer"></div>
|
|
181
|
+
{/if}
|
|
182
|
+
|
|
183
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
184
|
+
<div
|
|
185
|
+
class="wx-section-content"
|
|
186
|
+
bind:this={contentEls[section.name]}
|
|
187
|
+
use:drag={{
|
|
188
|
+
mode: section.mode,
|
|
189
|
+
dx: secDx,
|
|
190
|
+
dy: secDy,
|
|
191
|
+
xHeaders: section.xHeaders,
|
|
192
|
+
yHeaders: section.yHeaders,
|
|
193
|
+
sectionName: section.name,
|
|
194
|
+
model: $_view,
|
|
195
|
+
exec: api.exec,
|
|
196
|
+
getEvent: id => api.getEvent(id),
|
|
197
|
+
move: !readonly && !!section.ui?.drag,
|
|
198
|
+
clipDrag: section.ui?.clipDrag !== false,
|
|
199
|
+
create: !readonly && !!section.ui?.dragCreate,
|
|
200
|
+
}}
|
|
201
|
+
use:clickevent={{
|
|
202
|
+
exec: api.exec,
|
|
203
|
+
getEvent: id => api.getEvent(id),
|
|
204
|
+
onEventPopup: eventPopup ? overlay.handleEventPopup : undefined,
|
|
205
|
+
}}
|
|
206
|
+
use:clickdate={{ exec: api.exec }}
|
|
207
|
+
onmousemove={tooltip ? overlay.handleTooltipMove : undefined}
|
|
208
|
+
onmouseleave={tooltip ? overlay.handleTooltipLeave : undefined}
|
|
209
|
+
>
|
|
210
|
+
{#if section.ui?.dragCreate}
|
|
211
|
+
<div class="wx-drag-stub" data-drag-stub aria-hidden="true"></div>
|
|
212
|
+
{/if}
|
|
213
|
+
<SectionContent
|
|
214
|
+
{section}
|
|
215
|
+
dx={secDx}
|
|
216
|
+
dy={secDy}
|
|
217
|
+
scrollHeight={null}
|
|
218
|
+
measured={ready && !!sizes[section.name]}
|
|
219
|
+
{cellCss}
|
|
220
|
+
{eventCss}
|
|
221
|
+
{eventContent}
|
|
222
|
+
{view}
|
|
223
|
+
{tooltip}
|
|
224
|
+
onoverflow={section.mode === "grid" ? onGridOverflow : undefined}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
{/each}
|
|
229
|
+
|
|
230
|
+
{#if overlay.tooltipState && tooltip}
|
|
231
|
+
{@const TooltipCmp = tooltip}
|
|
232
|
+
<div
|
|
233
|
+
class="wx-calendar-tooltip"
|
|
234
|
+
style="position:fixed;left:{overlay.mousePos.x + 12}px;top:{overlay.mousePos.y + 16}px;z-index:10000;pointer-events:none"
|
|
235
|
+
aria-hidden="true"
|
|
236
|
+
>
|
|
237
|
+
<TooltipCmp event={overlay.tooltipState.event} />
|
|
238
|
+
</div>
|
|
239
|
+
{/if}
|
|
240
|
+
|
|
241
|
+
{#if overlay.eventPopupState && eventPopup}
|
|
242
|
+
{@const EventPopupCmp = eventPopup}
|
|
243
|
+
<Popup
|
|
244
|
+
at={overlay.eventPopupState.at}
|
|
245
|
+
parent={overlay.eventPopupState.element}
|
|
246
|
+
oncancel={overlay.hideEventPopup}
|
|
247
|
+
{...popupExtra}
|
|
248
|
+
>
|
|
249
|
+
<EventPopupCmp
|
|
250
|
+
event={overlay.eventPopupState.event}
|
|
251
|
+
close={overlay.hideEventPopup}
|
|
252
|
+
/>
|
|
253
|
+
</Popup>
|
|
254
|
+
{/if}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<style>
|
|
258
|
+
.wx-sections {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
height: 100%;
|
|
262
|
+
width: 100%;
|
|
263
|
+
overflow-x: hidden;
|
|
264
|
+
overflow-y: auto;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.wx-x-headers-row {
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-shrink: 0;
|
|
270
|
+
position: sticky;
|
|
271
|
+
top: 0;
|
|
272
|
+
z-index: 11;
|
|
273
|
+
background: var(--wx-background);
|
|
274
|
+
}
|
|
275
|
+
.wx-header-corner {
|
|
276
|
+
width: var(--wx-calendar-y-scale-width, 60px);
|
|
277
|
+
flex-shrink: 0;
|
|
278
|
+
border-right: var(--wx-border);
|
|
279
|
+
border-bottom: var(--wx-border);
|
|
280
|
+
}
|
|
281
|
+
.wx-x-headers-area {
|
|
282
|
+
flex: 1;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.wx-section {
|
|
287
|
+
display: grid;
|
|
288
|
+
grid-template-columns: 1fr;
|
|
289
|
+
overflow: hidden;
|
|
290
|
+
border-bottom: var(--wx-border);
|
|
291
|
+
}
|
|
292
|
+
.wx-section-last {
|
|
293
|
+
border-bottom: none;
|
|
294
|
+
}
|
|
295
|
+
.wx-section.wx-has-y-headers {
|
|
296
|
+
grid-template-columns: auto 1fr;
|
|
297
|
+
}
|
|
298
|
+
.wx-section-sticky {
|
|
299
|
+
position: sticky;
|
|
300
|
+
background: var(--wx-background);
|
|
301
|
+
}
|
|
302
|
+
.wx-section-grid {
|
|
303
|
+
overflow: visible;
|
|
304
|
+
min-height: 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.wx-y-headers-area {
|
|
308
|
+
align-self: stretch;
|
|
309
|
+
}
|
|
310
|
+
.wx-header-spacer {
|
|
311
|
+
width: var(--wx-calendar-y-scale-width, 60px);
|
|
312
|
+
border-right: var(--wx-border);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.wx-section-content {
|
|
316
|
+
min-width: 0;
|
|
317
|
+
position: relative;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.wx-sections :global(.wx-dragging),
|
|
321
|
+
.wx-sections :global(.wx-resizing) {
|
|
322
|
+
box-shadow: var(--wx-shadow-light) !important;
|
|
323
|
+
transition: none !important;
|
|
324
|
+
z-index: 100 !important;
|
|
325
|
+
pointer-events: none;
|
|
326
|
+
}
|
|
327
|
+
.wx-sections :global(.wx-dragging) {
|
|
328
|
+
cursor: grabbing !important;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
:global(.wx-drag-stub) {
|
|
332
|
+
display: none;
|
|
333
|
+
position: absolute;
|
|
334
|
+
background: var(--wx-color-primary);
|
|
335
|
+
opacity: 0.3;
|
|
336
|
+
border-radius: var(--wx-border-radius);
|
|
337
|
+
pointer-events: none;
|
|
338
|
+
z-index: 50;
|
|
339
|
+
}
|
|
340
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SectionResult, CellCss, EventCss } from "@svar-ui/calendar-store";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
data: SectionResult[];
|
|
4
|
+
cellCss?: CellCss;
|
|
5
|
+
eventCss?: EventCss;
|
|
6
|
+
eventContent?: any;
|
|
7
|
+
view: string;
|
|
8
|
+
tooltip?: any;
|
|
9
|
+
eventPopup?: any;
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare const Sections: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Sections = ReturnType<typeof Sections>;
|
|
14
|
+
export default Sections;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script lang="ts">import { getContext } from "svelte";
|
|
2
|
+
import { Popup } from "@svar-ui/svelte-core";
|
|
3
|
+
const _ = getContext("wx-i18n").getGroup("eventCalendar");
|
|
4
|
+
const { section, tooltip: TooltipCmp, eventContent } = $props();
|
|
5
|
+
const columns = $derived(section.ui?.columns ?? 3);
|
|
6
|
+
const weekStartDay = $derived(section.ui?.weekStartDay ?? 1);
|
|
7
|
+
const months = $derived(section.ui?.months ?? []);
|
|
8
|
+
const weekdayBase = [
|
|
9
|
+
"S",
|
|
10
|
+
"M",
|
|
11
|
+
"T",
|
|
12
|
+
"W",
|
|
13
|
+
"T",
|
|
14
|
+
"F",
|
|
15
|
+
"S"
|
|
16
|
+
];
|
|
17
|
+
const weekdays = $derived.by(() => {
|
|
18
|
+
const shift = (weekStartDay % 7 + 7) % 7;
|
|
19
|
+
const ordered = [];
|
|
20
|
+
for (let i = 0; i < 7; i++) {
|
|
21
|
+
const dow = (i + shift) % 7;
|
|
22
|
+
ordered.push({
|
|
23
|
+
label: weekdayBase[dow],
|
|
24
|
+
weekend: dow === 0 || dow === 6
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return ordered;
|
|
28
|
+
});
|
|
29
|
+
function dateStr(year, month, day) {
|
|
30
|
+
return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
31
|
+
}
|
|
32
|
+
function getDays(month) {
|
|
33
|
+
const days = [];
|
|
34
|
+
for (let i = 0; i < month.startOffset; i++) {
|
|
35
|
+
days.push({
|
|
36
|
+
day: 0,
|
|
37
|
+
empty: true,
|
|
38
|
+
today: false,
|
|
39
|
+
weekend: false,
|
|
40
|
+
hasEvents: false,
|
|
41
|
+
events: []
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (let d = 1; d <= month.totalDays; d++) {
|
|
45
|
+
const events = month.markedDays[d] || [];
|
|
46
|
+
const dow = new Date(month.year, month.month, d).getDay();
|
|
47
|
+
days.push({
|
|
48
|
+
day: d,
|
|
49
|
+
empty: false,
|
|
50
|
+
today: month.today === d,
|
|
51
|
+
weekend: dow === 0 || dow === 6,
|
|
52
|
+
hasEvents: events.length > 0,
|
|
53
|
+
events
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return days;
|
|
57
|
+
}
|
|
58
|
+
let tooltipData = $state(null);
|
|
59
|
+
function showTooltip(e, events) {
|
|
60
|
+
tooltipData = {
|
|
61
|
+
element: e.currentTarget,
|
|
62
|
+
events
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function hideTooltip() {
|
|
66
|
+
tooltipData = null;
|
|
67
|
+
}
|
|
68
|
+
function formatTime(date) {
|
|
69
|
+
return date.toLocaleTimeString(undefined, {
|
|
70
|
+
hour: "2-digit",
|
|
71
|
+
minute: "2-digit"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function formatRange(event) {
|
|
75
|
+
const { start, end } = event;
|
|
76
|
+
if (event.allDay || start.getFullYear() !== end.getFullYear() || start.getMonth() !== end.getMonth() || start.getDate() !== end.getDate()) {
|
|
77
|
+
return _("Full day");
|
|
78
|
+
}
|
|
79
|
+
return `${formatTime(start)} – ${formatTime(end)}`;
|
|
80
|
+
}
|
|
81
|
+
function eventTitle(event) {
|
|
82
|
+
return event.text || "";
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<div
|
|
87
|
+
class="wx-year-grid"
|
|
88
|
+
style="grid-template-columns: repeat({columns}, 1fr)"
|
|
89
|
+
>
|
|
90
|
+
{#each months as month (month.month)}
|
|
91
|
+
<div class="wx-year-month">
|
|
92
|
+
<div class="wx-month-label">{month.label}</div>
|
|
93
|
+
<div class="wx-month-grid">
|
|
94
|
+
{#each weekdays as wd}
|
|
95
|
+
<div class="wx-weekday-header" class:wx-weekend={wd.weekend}>
|
|
96
|
+
{wd.label}
|
|
97
|
+
</div>
|
|
98
|
+
{/each}
|
|
99
|
+
{#each getDays(month) as day}
|
|
100
|
+
{#if day.empty}
|
|
101
|
+
<div class="wx-month-day wx-empty"></div>
|
|
102
|
+
{:else}
|
|
103
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
104
|
+
<div
|
|
105
|
+
class="wx-month-day"
|
|
106
|
+
class:wx-today={day.today}
|
|
107
|
+
class:wx-weekend={day.weekend}
|
|
108
|
+
class:wx-has-events={day.hasEvents}
|
|
109
|
+
data-date={dateStr(month.year, month.month, day.day)}
|
|
110
|
+
aria-current={day.today ? "date" : undefined}
|
|
111
|
+
onmouseenter={day.hasEvents
|
|
112
|
+
? e => showTooltip(e, day.events)
|
|
113
|
+
: undefined}
|
|
114
|
+
onmouseleave={day.hasEvents
|
|
115
|
+
? hideTooltip
|
|
116
|
+
: undefined}
|
|
117
|
+
>
|
|
118
|
+
<span class="wx-day-num">{day.day}</span>
|
|
119
|
+
{#if day.hasEvents}
|
|
120
|
+
<span class="wx-event-dot" aria-hidden="true"></span>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
{/each}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{/each}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{#if tooltipData}
|
|
131
|
+
<Popup
|
|
132
|
+
parent={tooltipData.element}
|
|
133
|
+
at="bottom-start"
|
|
134
|
+
oncancel={hideTooltip}
|
|
135
|
+
>
|
|
136
|
+
<div
|
|
137
|
+
class="wx-year-tooltip"
|
|
138
|
+
class:wx-year-tooltip-custom={!!TooltipCmp}
|
|
139
|
+
>
|
|
140
|
+
{#if TooltipCmp}
|
|
141
|
+
<TooltipCmp events={tooltipData.events} />
|
|
142
|
+
{:else}
|
|
143
|
+
{#each tooltipData.events as ev}
|
|
144
|
+
<div class="wx-tooltip-event">
|
|
145
|
+
{#if eventContent}
|
|
146
|
+
{@const EventContentCmp = eventContent}
|
|
147
|
+
<div class="wx-tooltip-event-content">
|
|
148
|
+
<EventContentCmp event={ev} mode="year-tooltip" />
|
|
149
|
+
</div>
|
|
150
|
+
{:else}
|
|
151
|
+
<span class="wx-tooltip-time">
|
|
152
|
+
{formatRange(ev)}
|
|
153
|
+
</span>
|
|
154
|
+
<span class="wx-tooltip-title">{eventTitle(ev)}</span>
|
|
155
|
+
{/if}
|
|
156
|
+
</div>
|
|
157
|
+
{/each}
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
160
|
+
</Popup>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
<style>
|
|
164
|
+
.wx-year-grid {
|
|
165
|
+
display: grid;
|
|
166
|
+
gap: 16px;
|
|
167
|
+
padding: 16px;
|
|
168
|
+
position: relative;
|
|
169
|
+
}
|
|
170
|
+
.wx-month-label {
|
|
171
|
+
font-weight: var(--wx-font-weight-md);
|
|
172
|
+
margin-bottom: 4px;
|
|
173
|
+
padding-left: 20px;
|
|
174
|
+
}
|
|
175
|
+
.wx-month-grid {
|
|
176
|
+
display: grid;
|
|
177
|
+
grid-template-columns: repeat(7, 1fr);
|
|
178
|
+
gap: 1px;
|
|
179
|
+
}
|
|
180
|
+
.wx-weekday-header {
|
|
181
|
+
text-align: center;
|
|
182
|
+
font-size: 10px;
|
|
183
|
+
color: var(--wx-color-font-alt);
|
|
184
|
+
padding: 2px 0;
|
|
185
|
+
}
|
|
186
|
+
.wx-month-day {
|
|
187
|
+
text-align: center;
|
|
188
|
+
padding: 2px;
|
|
189
|
+
position: relative;
|
|
190
|
+
min-height: 24px;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
align-items: center;
|
|
194
|
+
cursor: default;
|
|
195
|
+
color: var(--wx-color-font-alt);
|
|
196
|
+
}
|
|
197
|
+
.wx-month-day.wx-weekend,
|
|
198
|
+
.wx-weekday-header.wx-weekend {
|
|
199
|
+
background-color: var(--wx-calendar-weekend-background);
|
|
200
|
+
}
|
|
201
|
+
.wx-month-day.wx-has-events {
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
color: var(--wx-color-font);
|
|
204
|
+
}
|
|
205
|
+
.wx-day-num {
|
|
206
|
+
font-size: 13px;
|
|
207
|
+
}
|
|
208
|
+
.wx-month-day.wx-today .wx-day-num {
|
|
209
|
+
background: var(--wx-color-primary);
|
|
210
|
+
color: var(--wx-color-primary-font);
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
width: 20px;
|
|
213
|
+
height: 20px;
|
|
214
|
+
line-height: 20px;
|
|
215
|
+
display: inline-block;
|
|
216
|
+
}
|
|
217
|
+
.wx-event-dot {
|
|
218
|
+
display: block;
|
|
219
|
+
width: 4px;
|
|
220
|
+
height: 4px;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
background: var(--wx-color-primary);
|
|
223
|
+
margin-top: 1px;
|
|
224
|
+
}
|
|
225
|
+
.wx-year-tooltip {
|
|
226
|
+
background: var(--wx-background);
|
|
227
|
+
border: var(--wx-border);
|
|
228
|
+
border-radius: var(--wx-border-radius);
|
|
229
|
+
padding: 8px 12px;
|
|
230
|
+
box-shadow: var(--wx-shadow-light);
|
|
231
|
+
min-width: 150px;
|
|
232
|
+
max-width: 300px;
|
|
233
|
+
}
|
|
234
|
+
.wx-year-tooltip-custom {
|
|
235
|
+
padding: 0;
|
|
236
|
+
border: none;
|
|
237
|
+
background: none;
|
|
238
|
+
box-shadow: none;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
max-width: none;
|
|
241
|
+
}
|
|
242
|
+
.wx-tooltip-event {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
padding: 2px 0;
|
|
246
|
+
font-size: var(--wx-font-size-sm);
|
|
247
|
+
}
|
|
248
|
+
.wx-tooltip-event + .wx-tooltip-event {
|
|
249
|
+
border-top: var(--wx-border);
|
|
250
|
+
padding-top: 4px;
|
|
251
|
+
margin-top: 2px;
|
|
252
|
+
}
|
|
253
|
+
.wx-tooltip-time {
|
|
254
|
+
flex-shrink: 0;
|
|
255
|
+
color: var(--wx-color-font-alt);
|
|
256
|
+
margin-right: 8px;
|
|
257
|
+
}
|
|
258
|
+
.wx-tooltip-title {
|
|
259
|
+
color: var(--wx-color-font);
|
|
260
|
+
}
|
|
261
|
+
.wx-tooltip-event-content {
|
|
262
|
+
flex: 1;
|
|
263
|
+
min-width: 0;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SectionResult } from "@svar-ui/calendar-store";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
section: SectionResult;
|
|
4
|
+
tooltip?: any;
|
|
5
|
+
eventContent?: any;
|
|
6
|
+
};
|
|
7
|
+
declare const YearSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type YearSection = ReturnType<typeof YearSection>;
|
|
9
|
+
export default YearSection;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EventPopupInfo } from "../../directives/clickevent.js";
|
|
2
|
+
import type { TPosition } from "@svar-ui/lib-dom";
|
|
3
|
+
import type { EventID } from "@svar-ui/calendar-store";
|
|
4
|
+
export declare function useEventOverlay(getEvent: (id: EventID) => any, getEventPopupAt: (element: HTMLElement) => TPosition): {
|
|
5
|
+
readonly tooltipState: {
|
|
6
|
+
event: any;
|
|
7
|
+
} | null;
|
|
8
|
+
readonly mousePos: {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
};
|
|
12
|
+
readonly eventPopupState: {
|
|
13
|
+
event: any;
|
|
14
|
+
element: HTMLElement;
|
|
15
|
+
at: TPosition;
|
|
16
|
+
} | null;
|
|
17
|
+
handleTooltipMove: (e: MouseEvent) => void;
|
|
18
|
+
handleTooltipLeave: () => void;
|
|
19
|
+
handleEventPopup: (info: EventPopupInfo | null) => void;
|
|
20
|
+
hideEventPopup: () => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getID, locate } from "@svar-ui/lib-dom";
|
|
2
|
+
export function useEventOverlay(getEvent, getEventPopupAt) {
|
|
3
|
+
let _tooltipTarget = null;
|
|
4
|
+
let tooltipState = $state(null);
|
|
5
|
+
let mousePos = $state({ x: 0, y: 0 });
|
|
6
|
+
let eventPopupState = $state(null);
|
|
7
|
+
function handleTooltipMove(e) {
|
|
8
|
+
mousePos = { x: e.clientX, y: e.clientY };
|
|
9
|
+
if (eventPopupState || !e.target)
|
|
10
|
+
return;
|
|
11
|
+
const el = locate(e.target) ?? null;
|
|
12
|
+
if (el === _tooltipTarget)
|
|
13
|
+
return;
|
|
14
|
+
_tooltipTarget = el;
|
|
15
|
+
if (!el) {
|
|
16
|
+
tooltipState = null;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const ev = getEvent(getID(el));
|
|
20
|
+
tooltipState = ev ? { event: ev } : null;
|
|
21
|
+
}
|
|
22
|
+
function handleTooltipLeave() {
|
|
23
|
+
_tooltipTarget = null;
|
|
24
|
+
tooltipState = null;
|
|
25
|
+
}
|
|
26
|
+
function handleEventPopup(info) {
|
|
27
|
+
tooltipState = null;
|
|
28
|
+
_tooltipTarget = null;
|
|
29
|
+
if (!info) {
|
|
30
|
+
eventPopupState = null;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const ev = getEvent(info.eventId);
|
|
34
|
+
if (ev) {
|
|
35
|
+
eventPopupState = {
|
|
36
|
+
event: ev,
|
|
37
|
+
element: info.element,
|
|
38
|
+
at: getEventPopupAt(info.element),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function hideEventPopup() {
|
|
43
|
+
eventPopupState = null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
get tooltipState() {
|
|
47
|
+
return tooltipState;
|
|
48
|
+
},
|
|
49
|
+
get mousePos() {
|
|
50
|
+
return mousePos;
|
|
51
|
+
},
|
|
52
|
+
get eventPopupState() {
|
|
53
|
+
return eventPopupState;
|
|
54
|
+
},
|
|
55
|
+
handleTooltipMove,
|
|
56
|
+
handleTooltipLeave,
|
|
57
|
+
handleEventPopup,
|
|
58
|
+
hideEventPopup,
|
|
59
|
+
};
|
|
60
|
+
}
|