picocrank 1.26.0 → 1.27.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/eslint.config.js +12 -5
- package/package.json +8 -5
- package/styles/picocrank-extensions.css +46 -0
- package/vite.config.js +5 -0
- package/vue/components/Calendar.vue +125 -128
- package/vue/components/FormField.spec.js +350 -0
- package/vue/components/FormField.vue +236 -32
- package/vue/components/FormLayout.spec.js +56 -0
- package/vue/components/Login.vue +152 -41
- package/vue/components/Pagination.vue +5 -4
- package/vue/components/Tabs.spec.js +101 -0
- package/vue/components/Tabs.vue +115 -25
- package/vue/components/form-validation.spec.js +126 -0
- package/vue/examples/FormExample.vue +76 -48
- package/vue/examples/Login.vue +6 -1
- package/vue/examples/LoginExample.vue +98 -2
- package/vue/examples/PageStructure.vue +9 -9
- package/vue/examples/TableExample.vue +5 -5
- package/vue/examples/TabsExample.vue +41 -7
- package/vue/test/formTestUtils.js +279 -0
- package/vue/test/setup.js +5 -0
package/eslint.config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import js from '@eslint/js'
|
|
2
2
|
import pluginVue from 'eslint-plugin-vue'
|
|
3
3
|
import globals from 'globals'
|
|
4
|
-
import tseslint from 'typescript-eslint'
|
|
5
4
|
import vueParser from 'vue-eslint-parser'
|
|
6
5
|
|
|
7
6
|
export default [
|
|
@@ -50,15 +49,23 @@ export default [
|
|
|
50
49
|
languageOptions: {
|
|
51
50
|
parser: vueParser,
|
|
52
51
|
parserOptions: {
|
|
53
|
-
parser: {
|
|
54
|
-
js: 'espree',
|
|
55
|
-
ts: tseslint.parser,
|
|
56
|
-
},
|
|
57
52
|
sourceType: 'module',
|
|
58
53
|
extraFileExtensions: ['.vue'],
|
|
59
54
|
},
|
|
60
55
|
},
|
|
61
56
|
},
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.spec.js', 'vue/test/**'],
|
|
59
|
+
languageOptions: {
|
|
60
|
+
globals: {
|
|
61
|
+
...globals.browser,
|
|
62
|
+
...globals.node,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
rules: {
|
|
66
|
+
'vue/one-component-per-file': 'off',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
62
69
|
{
|
|
63
70
|
files: ['**/*.vue'],
|
|
64
71
|
rules: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
6
6
|
"main": "vite.config.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"preview": "vite preview",
|
|
12
12
|
"lint": "eslint .",
|
|
13
13
|
"lint:fix": "eslint . --fix",
|
|
14
|
-
"test": "
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"@hugeicons/core-free-icons": "^4.3.0",
|
|
27
28
|
"@hugeicons/vue": "^1.0.8",
|
|
28
29
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
29
|
-
"femtocrank": "^2.7.
|
|
30
|
+
"femtocrank": "^2.7.1",
|
|
30
31
|
"unplugin-vue-components": "^32.1.0",
|
|
31
32
|
"vite": "^8.2.2",
|
|
32
33
|
"vue": "^3.5.41",
|
|
@@ -34,12 +35,14 @@
|
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@eslint/js": "^10.0.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
39
|
+
"@vue/test-utils": "^2.4.11",
|
|
37
40
|
"eslint": "^10.9.1",
|
|
38
41
|
"eslint-plugin-vue": "^10.10.0",
|
|
39
42
|
"globals": "^17.11.0",
|
|
43
|
+
"happy-dom": "^20.11.8",
|
|
40
44
|
"semantic-release": "^25.0.9",
|
|
41
|
-
"
|
|
42
|
-
"typescript-eslint": "^8.68.0",
|
|
45
|
+
"vitest": "^4.1.11",
|
|
43
46
|
"vue-eslint-parser": "^10.4.1"
|
|
44
47
|
},
|
|
45
48
|
"homepage": "https://github.com/jamesread/picocrank#readme"
|
|
@@ -35,8 +35,15 @@
|
|
|
35
35
|
--login-error-fg: var(--karma-on-bg-fg);
|
|
36
36
|
--login-muted-fg: var(--muted-text-color);
|
|
37
37
|
|
|
38
|
+
--login-spinner-fg: var(--link-color);
|
|
39
|
+
--login-spinner-track: var(--border-color);
|
|
40
|
+
|
|
41
|
+
--form-field-required-fg: var(--karma-bad);
|
|
42
|
+
|
|
38
43
|
--table-muted-fg: var(--muted-text-color);
|
|
39
44
|
|
|
45
|
+
--button-transparent-disabled-fg: color-mix(in srgb, var(--text-color) 45%, var(--section-bg-color));
|
|
46
|
+
|
|
40
47
|
--table-popover-bg: var(--section-bg-color);
|
|
41
48
|
--table-popover-border: var(--border-color);
|
|
42
49
|
--table-popover-shadow: 0 8px 24px color-mix(in srgb, var(--shadow-color) 60%, transparent);
|
|
@@ -106,6 +113,45 @@ section .section-header [role="toolbar"] .button {
|
|
|
106
113
|
min-width: 0;
|
|
107
114
|
line-height: normal;
|
|
108
115
|
}
|
|
116
|
+
|
|
117
|
+
section.narrow {
|
|
118
|
+
max-width: 480px;
|
|
119
|
+
margin-left: auto;
|
|
120
|
+
margin-right: auto;
|
|
121
|
+
border-radius: .8em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
section.narrow form {
|
|
125
|
+
grid-template-columns: 1fr;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
section.narrow fieldset {
|
|
129
|
+
grid-column: span 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
form > .form-field-label-skip {
|
|
133
|
+
grid-column: 1 / -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* FormField applies .field-invalid when an error is set programmatically.
|
|
138
|
+
* Mirrors femtocrank :user-invalid (which only applies after user interaction).
|
|
139
|
+
*/
|
|
140
|
+
input.field-invalid,
|
|
141
|
+
textarea.field-invalid,
|
|
142
|
+
select.field-invalid {
|
|
143
|
+
border-color: var(--karma-bad-border);
|
|
144
|
+
background-color: var(--karma-bad-wash);
|
|
145
|
+
color: var(--text-color);
|
|
146
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--karma-bad) 35%, transparent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
input.field-invalid:focus-visible,
|
|
150
|
+
textarea.field-invalid:focus-visible,
|
|
151
|
+
select.field-invalid:focus-visible {
|
|
152
|
+
outline: 2px solid var(--karma-bad-border);
|
|
153
|
+
outline-offset: 2px;
|
|
154
|
+
}
|
|
109
155
|
}
|
|
110
156
|
|
|
111
157
|
/*
|
package/vite.config.js
CHANGED
|
@@ -1,84 +1,79 @@
|
|
|
1
|
-
<script setup
|
|
1
|
+
<script setup>
|
|
2
2
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
3
3
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
error: null,
|
|
66
|
-
eventDragEnabled: true,
|
|
67
|
-
eventMoveResponseTimeoutMs: 15000,
|
|
68
|
-
showNavigation: true,
|
|
69
|
-
shortMonthSuffix: false,
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
events: {
|
|
7
|
+
type: Array,
|
|
8
|
+
required: true,
|
|
9
|
+
},
|
|
10
|
+
monthNames: {
|
|
11
|
+
type: Array,
|
|
12
|
+
default: () => [
|
|
13
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
14
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
dayNames: {
|
|
18
|
+
type: Array,
|
|
19
|
+
default: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
20
|
+
},
|
|
21
|
+
loading: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
error: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: null,
|
|
28
|
+
},
|
|
29
|
+
eventDragEnabled: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: true,
|
|
32
|
+
},
|
|
33
|
+
eventMoveResponseTimeoutMs: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: 15000,
|
|
36
|
+
},
|
|
37
|
+
getEventDate: {
|
|
38
|
+
type: Function,
|
|
39
|
+
default: null,
|
|
40
|
+
},
|
|
41
|
+
getEventDateRange: {
|
|
42
|
+
type: Function,
|
|
43
|
+
default: null,
|
|
44
|
+
},
|
|
45
|
+
formatEventTime: {
|
|
46
|
+
type: Function,
|
|
47
|
+
default: null,
|
|
48
|
+
},
|
|
49
|
+
showNavigation: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
53
|
+
shortMonthSuffix: {
|
|
54
|
+
type: Boolean,
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
currentMonth: {
|
|
58
|
+
type: Number,
|
|
59
|
+
default: undefined,
|
|
60
|
+
},
|
|
61
|
+
currentYear: {
|
|
62
|
+
type: Number,
|
|
63
|
+
default: undefined,
|
|
64
|
+
},
|
|
70
65
|
})
|
|
71
66
|
|
|
72
|
-
const emit = defineEmits
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
const emit = defineEmits([
|
|
68
|
+
'event-click',
|
|
69
|
+
'date-click',
|
|
70
|
+
'date-range-select',
|
|
71
|
+
'month-change',
|
|
72
|
+
'event-context-menu',
|
|
73
|
+
'event-move-request',
|
|
74
|
+
'event-moved',
|
|
75
|
+
'event-move-rejected',
|
|
76
|
+
])
|
|
82
77
|
|
|
83
78
|
// Internal calendar state
|
|
84
79
|
const internalCurrentDate = ref(new Date())
|
|
@@ -93,7 +88,7 @@ const viewMonth = computed(() => currentDate.value.getMonth())
|
|
|
93
88
|
const viewYear = computed(() => currentDate.value.getFullYear())
|
|
94
89
|
|
|
95
90
|
// Compute the first visible day (Monday) for the current month view
|
|
96
|
-
function getStartOfGrid(date
|
|
91
|
+
function getStartOfGrid(date) {
|
|
97
92
|
const firstOfMonth = new Date(date.getFullYear(), date.getMonth(), 1)
|
|
98
93
|
const dayOfWeek = (firstOfMonth.getDay() + 6) % 7 // Monday=0
|
|
99
94
|
const start = new Date(firstOfMonth)
|
|
@@ -103,19 +98,19 @@ function getStartOfGrid(date: Date): Date {
|
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
// Anchor date for the 6x7 grid; we will shift this by weeks without re-rendering the grid structure
|
|
106
|
-
const gridStartDate = ref
|
|
101
|
+
const gridStartDate = ref(getStartOfGrid(currentDate.value))
|
|
107
102
|
// Track if we're actively scrolling to prevent watcher from resetting grid anchor
|
|
108
103
|
const isScrolling = ref(false)
|
|
109
104
|
|
|
110
105
|
// Helper function to check if a time is midnight (00:00)
|
|
111
|
-
function isMidnight(dateValue
|
|
106
|
+
function isMidnight(dateValue) {
|
|
112
107
|
if (!dateValue) return false
|
|
113
108
|
const date = new Date(dateValue)
|
|
114
109
|
return date.getHours() === 0 && date.getMinutes() === 0
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
// Helper function to format time, returning "No time" if midnight
|
|
118
|
-
function formatTimeOrNoTime(dateValue
|
|
113
|
+
function formatTimeOrNoTime(dateValue) {
|
|
119
114
|
if (!dateValue) return 'No time'
|
|
120
115
|
if (isMidnight(dateValue)) return 'No time'
|
|
121
116
|
const date = new Date(dateValue)
|
|
@@ -123,7 +118,7 @@ function formatTimeOrNoTime(dateValue: any): string {
|
|
|
123
118
|
}
|
|
124
119
|
|
|
125
120
|
// Determine if a date is in the past (before today)
|
|
126
|
-
function isPastDay(date
|
|
121
|
+
function isPastDay(date) {
|
|
127
122
|
const day = new Date(date)
|
|
128
123
|
day.setHours(0, 0, 0, 0)
|
|
129
124
|
const today = new Date()
|
|
@@ -132,14 +127,14 @@ function isPastDay(date: Date): boolean {
|
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
// Get event date range
|
|
135
|
-
function resolveEventDateRange(event
|
|
130
|
+
function resolveEventDateRange(event) {
|
|
136
131
|
if (props.getEventDateRange) {
|
|
137
132
|
return props.getEventDateRange(event)
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
// Default implementation
|
|
141
|
-
let start
|
|
142
|
-
let end
|
|
136
|
+
let start = null
|
|
137
|
+
let end = null
|
|
143
138
|
|
|
144
139
|
if (event.startDate) {
|
|
145
140
|
start = new Date(event.startDate)
|
|
@@ -155,7 +150,7 @@ function resolveEventDateRange(event: CalendarEvent): { start: Date | null; end:
|
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
// Get events for a specific date
|
|
158
|
-
function getEventsForDate(date
|
|
153
|
+
function getEventsForDate(date) {
|
|
159
154
|
const targetDate = new Date(date)
|
|
160
155
|
targetDate.setHours(0, 0, 0, 0)
|
|
161
156
|
|
|
@@ -178,7 +173,7 @@ function getEventsForDate(date: Date): CalendarEvent[] {
|
|
|
178
173
|
}
|
|
179
174
|
|
|
180
175
|
// Check if an event is a multi-day event
|
|
181
|
-
function isMultiDayEvent(event
|
|
176
|
+
function isMultiDayEvent(event) {
|
|
182
177
|
const { start, end } = resolveEventDateRange(event)
|
|
183
178
|
|
|
184
179
|
if (!start || !end) return false
|
|
@@ -193,7 +188,7 @@ function isMultiDayEvent(event: CalendarEvent): boolean {
|
|
|
193
188
|
}
|
|
194
189
|
|
|
195
190
|
// Get the position of an event within a multi-day range for a specific date
|
|
196
|
-
function getMultiDayPosition(event
|
|
191
|
+
function getMultiDayPosition(event, date) {
|
|
197
192
|
const { start, end } = resolveEventDateRange(event)
|
|
198
193
|
|
|
199
194
|
if (!start || !end) return 'single'
|
|
@@ -215,7 +210,7 @@ function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middl
|
|
|
215
210
|
}
|
|
216
211
|
|
|
217
212
|
// Format event time based on position in multi-day event
|
|
218
|
-
function formatEventTimeDefault(event
|
|
213
|
+
function formatEventTimeDefault(event, date) {
|
|
219
214
|
if (props.formatEventTime) {
|
|
220
215
|
return props.formatEventTime(event, date)
|
|
221
216
|
}
|
|
@@ -238,8 +233,8 @@ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
|
|
|
238
233
|
return 'All day'
|
|
239
234
|
}
|
|
240
235
|
|
|
241
|
-
function getEventStyle(event
|
|
242
|
-
const style
|
|
236
|
+
function getEventStyle(event) {
|
|
237
|
+
const style = {}
|
|
243
238
|
if (event.color) {
|
|
244
239
|
style['--event-color'] = event.color
|
|
245
240
|
}
|
|
@@ -250,7 +245,7 @@ function getEventStyle(event: CalendarEvent): Record<string, string> | undefined
|
|
|
250
245
|
}
|
|
251
246
|
|
|
252
247
|
// Get ordinal suffix for a number (1st, 2nd, 3rd, 4th, etc.)
|
|
253
|
-
function getOrdinalSuffix(day
|
|
248
|
+
function getOrdinalSuffix(day) {
|
|
254
249
|
const j = day % 10
|
|
255
250
|
const k = day % 100
|
|
256
251
|
if (j === 1 && k !== 11) {
|
|
@@ -267,7 +262,7 @@ function getOrdinalSuffix(day: number): string {
|
|
|
267
262
|
|
|
268
263
|
// Calendar generation from persistent grid start
|
|
269
264
|
const calendarDays = computed(() => {
|
|
270
|
-
const days = []
|
|
265
|
+
const days = []
|
|
271
266
|
const start = gridStartDate.value
|
|
272
267
|
for (let i = 0; i < 42; i++) {
|
|
273
268
|
const date = new Date(start)
|
|
@@ -320,13 +315,13 @@ function goToToday() {
|
|
|
320
315
|
// Week navigation (mouse wheel)
|
|
321
316
|
const lastWheelAt = ref(0)
|
|
322
317
|
const wheelThrottleMs = 180
|
|
323
|
-
function getVisibleMonthYear(start
|
|
318
|
+
function getVisibleMonthYear(start) {
|
|
324
319
|
const mid = new Date(start)
|
|
325
320
|
mid.setDate(start.getDate() + 21) // middle of 6x7 grid
|
|
326
321
|
return { month: mid.getMonth(), year: mid.getFullYear() }
|
|
327
322
|
}
|
|
328
323
|
|
|
329
|
-
function adjustByWeeks(weeks
|
|
324
|
+
function adjustByWeeks(weeks) {
|
|
330
325
|
const newStart = new Date(gridStartDate.value)
|
|
331
326
|
newStart.setDate(newStart.getDate() + weeks * 7)
|
|
332
327
|
gridStartDate.value = newStart
|
|
@@ -347,7 +342,7 @@ function adjustByWeeks(weeks: number): void {
|
|
|
347
342
|
}
|
|
348
343
|
}
|
|
349
344
|
|
|
350
|
-
function handleWheel(event
|
|
345
|
+
function handleWheel(event) {
|
|
351
346
|
// Prevent page scrolling while using calendar scroll
|
|
352
347
|
event.preventDefault()
|
|
353
348
|
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()
|
|
@@ -358,11 +353,11 @@ function handleWheel(event: WheelEvent): void {
|
|
|
358
353
|
}
|
|
359
354
|
|
|
360
355
|
// Event handlers
|
|
361
|
-
function handleEventClick(event
|
|
356
|
+
function handleEventClick(event) {
|
|
362
357
|
emit('event-click', event)
|
|
363
358
|
}
|
|
364
359
|
|
|
365
|
-
function handleContextMenu(event
|
|
360
|
+
function handleContextMenu(event, mouseEvent) {
|
|
366
361
|
mouseEvent.preventDefault()
|
|
367
362
|
mouseEvent.stopPropagation()
|
|
368
363
|
emit('event-context-menu', event, mouseEvent)
|
|
@@ -370,34 +365,34 @@ function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
|
|
|
370
365
|
|
|
371
366
|
// --- Date range selection (click / drag) ---
|
|
372
367
|
|
|
373
|
-
function startOfDay(d
|
|
368
|
+
function startOfDay(d) {
|
|
374
369
|
const x = new Date(d)
|
|
375
370
|
x.setHours(0, 0, 0, 0)
|
|
376
371
|
return x
|
|
377
372
|
}
|
|
378
373
|
|
|
379
|
-
function toDateKey(d
|
|
374
|
+
function toDateKey(d) {
|
|
380
375
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
381
376
|
}
|
|
382
377
|
|
|
383
|
-
function dateFromKey(key
|
|
378
|
+
function dateFromKey(key) {
|
|
384
379
|
const [y, m, day] = key.split('-').map(Number)
|
|
385
380
|
return startOfDay(new Date(y, m - 1, day))
|
|
386
381
|
}
|
|
387
382
|
|
|
388
383
|
const isDraggingRange = ref(false)
|
|
389
|
-
const rangeDragAnchor = ref
|
|
390
|
-
const rangeDragEnd = ref
|
|
391
|
-
const selectedRangeStart = ref
|
|
392
|
-
const selectedRangeEnd = ref
|
|
384
|
+
const rangeDragAnchor = ref(null)
|
|
385
|
+
const rangeDragEnd = ref(null)
|
|
386
|
+
const selectedRangeStart = ref(null)
|
|
387
|
+
const selectedRangeEnd = ref(null)
|
|
393
388
|
|
|
394
|
-
function isEventTarget(ev
|
|
389
|
+
function isEventTarget(ev) {
|
|
395
390
|
const t = ev.target
|
|
396
391
|
if (!(t instanceof Node)) return false
|
|
397
|
-
return Boolean(
|
|
392
|
+
return Boolean(t.closest?.('.calendar-event'))
|
|
398
393
|
}
|
|
399
394
|
|
|
400
|
-
function getRangeBounds()
|
|
395
|
+
function getRangeBounds() {
|
|
401
396
|
const a = rangeDragAnchor.value
|
|
402
397
|
const b = rangeDragEnd.value
|
|
403
398
|
if (!a || !b) return null
|
|
@@ -407,15 +402,17 @@ function getRangeBounds(): { lo: Date; hi: Date } | null {
|
|
|
407
402
|
return { lo: startOfDay(b), hi: startOfDay(a) }
|
|
408
403
|
}
|
|
409
404
|
|
|
410
|
-
function rangeHighlightForDate(date
|
|
405
|
+
function rangeHighlightForDate(date) {
|
|
411
406
|
const bounds = isDraggingRange.value
|
|
412
407
|
? getRangeBounds()
|
|
413
408
|
: selectedRangeStart.value && selectedRangeEnd.value
|
|
414
409
|
? (() => {
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const
|
|
410
|
+
const rangeStart = selectedRangeStart.value
|
|
411
|
+
const rangeEnd = selectedRangeEnd.value
|
|
412
|
+
const sa = startOfDay(rangeStart).getTime()
|
|
413
|
+
const sb = startOfDay(rangeEnd).getTime()
|
|
414
|
+
const lo = sa <= sb ? startOfDay(rangeStart) : startOfDay(rangeEnd)
|
|
415
|
+
const hi = sa <= sb ? startOfDay(rangeEnd) : startOfDay(rangeStart)
|
|
419
416
|
return { lo, hi }
|
|
420
417
|
})()
|
|
421
418
|
: null
|
|
@@ -428,18 +425,18 @@ function rangeHighlightForDate(date: Date): 'none' | 'single' | 'start' | 'end'
|
|
|
428
425
|
return 'middle'
|
|
429
426
|
}
|
|
430
427
|
|
|
431
|
-
function onWindowTouchMove(ev
|
|
428
|
+
function onWindowTouchMove(ev) {
|
|
432
429
|
if (!isDraggingRange.value || ev.touches.length === 0) return
|
|
433
430
|
const { clientX, clientY } = ev.touches[0]
|
|
434
431
|
const el = document.elementFromPoint(clientX, clientY)
|
|
435
432
|
if (!el) return
|
|
436
|
-
const cell =
|
|
433
|
+
const cell = el.closest?.('[data-calendar-date]')
|
|
437
434
|
const key = cell?.dataset?.calendarDate
|
|
438
435
|
if (!key) return
|
|
439
436
|
rangeDragEnd.value = dateFromKey(key)
|
|
440
437
|
}
|
|
441
438
|
|
|
442
|
-
function detachRangePointerListeners()
|
|
439
|
+
function detachRangePointerListeners() {
|
|
443
440
|
window.removeEventListener('mouseup', endRangeSelectionFromPointer)
|
|
444
441
|
window.removeEventListener('blur', endRangeSelectionFromPointer)
|
|
445
442
|
window.removeEventListener('touchmove', onWindowTouchMove)
|
|
@@ -447,7 +444,7 @@ function detachRangePointerListeners(): void {
|
|
|
447
444
|
window.removeEventListener('touchcancel', endRangeSelectionFromPointer)
|
|
448
445
|
}
|
|
449
446
|
|
|
450
|
-
function endRangeSelectionFromPointer()
|
|
447
|
+
function endRangeSelectionFromPointer() {
|
|
451
448
|
if (!isDraggingRange.value) return
|
|
452
449
|
const bounds = getRangeBounds()
|
|
453
450
|
isDraggingRange.value = false
|
|
@@ -466,7 +463,7 @@ function endRangeSelectionFromPointer(): void {
|
|
|
466
463
|
}
|
|
467
464
|
}
|
|
468
465
|
|
|
469
|
-
function onDayPointerDown(date
|
|
466
|
+
function onDayPointerDown(date, ev) {
|
|
470
467
|
if (isEventTarget(ev)) return
|
|
471
468
|
if (ev instanceof MouseEvent && ev.button !== 0) return
|
|
472
469
|
|
|
@@ -484,7 +481,7 @@ function onDayPointerDown(date: Date, ev: MouseEvent | TouchEvent): void {
|
|
|
484
481
|
}
|
|
485
482
|
}
|
|
486
483
|
|
|
487
|
-
function onDayPointerEnter(date
|
|
484
|
+
function onDayPointerEnter(date) {
|
|
488
485
|
if (!isDraggingRange.value) return
|
|
489
486
|
rangeDragEnd.value = startOfDay(date)
|
|
490
487
|
}
|
|
@@ -497,17 +494,17 @@ onUnmounted(() => {
|
|
|
497
494
|
|
|
498
495
|
const CALENDAR_EVENT_DRAG_MIME = 'application/x-picocrank-calendar-event-id'
|
|
499
496
|
|
|
500
|
-
const draggingEventId = ref
|
|
501
|
-
const draggingSourceDate = ref
|
|
502
|
-
const dragOverTargetDate = ref
|
|
497
|
+
const draggingEventId = ref(null)
|
|
498
|
+
const draggingSourceDate = ref(null)
|
|
499
|
+
const dragOverTargetDate = ref(null)
|
|
503
500
|
const eventMoveLocked = ref(false)
|
|
504
501
|
|
|
505
|
-
function sameCalendarDay(a
|
|
502
|
+
function sameCalendarDay(a, b) {
|
|
506
503
|
if (!a || !b) return false
|
|
507
504
|
return startOfDay(a).getTime() === startOfDay(b).getTime()
|
|
508
505
|
}
|
|
509
506
|
|
|
510
|
-
function onEventDragStart(event
|
|
507
|
+
function onEventDragStart(event, sourceCellDate, dragEvent) {
|
|
511
508
|
if (!props.eventDragEnabled) {
|
|
512
509
|
dragEvent.preventDefault()
|
|
513
510
|
return
|
|
@@ -521,13 +518,13 @@ function onEventDragStart(event: CalendarEvent, sourceCellDate: Date, dragEvent:
|
|
|
521
518
|
}
|
|
522
519
|
}
|
|
523
520
|
|
|
524
|
-
function onEventDragEnd()
|
|
521
|
+
function onEventDragEnd() {
|
|
525
522
|
draggingEventId.value = null
|
|
526
523
|
draggingSourceDate.value = null
|
|
527
524
|
dragOverTargetDate.value = null
|
|
528
525
|
}
|
|
529
526
|
|
|
530
|
-
function onDayDragOver(dayDate
|
|
527
|
+
function onDayDragOver(dayDate, ev) {
|
|
531
528
|
if (!props.eventDragEnabled || draggingEventId.value === null) return
|
|
532
529
|
ev.preventDefault()
|
|
533
530
|
if (ev.dataTransfer) {
|
|
@@ -536,7 +533,7 @@ function onDayDragOver(dayDate: Date, ev: DragEvent): void {
|
|
|
536
533
|
dragOverTargetDate.value = startOfDay(dayDate)
|
|
537
534
|
}
|
|
538
535
|
|
|
539
|
-
function onDayDrop(dayDate
|
|
536
|
+
function onDayDrop(dayDate, ev) {
|
|
540
537
|
ev.preventDefault()
|
|
541
538
|
dragOverTargetDate.value = null
|
|
542
539
|
if (!props.eventDragEnabled) return
|
|
@@ -560,9 +557,9 @@ function onDayDrop(dayDate: Date, ev: DragEvent): void {
|
|
|
560
557
|
const targetClone = new Date(targetDate)
|
|
561
558
|
|
|
562
559
|
let settled = false
|
|
563
|
-
let watchdog
|
|
560
|
+
let watchdog
|
|
564
561
|
|
|
565
|
-
function respond(accepted
|
|
562
|
+
function respond(accepted) {
|
|
566
563
|
if (settled) return
|
|
567
564
|
settled = true
|
|
568
565
|
if (watchdog !== undefined) {
|