@vue-skuilder/common-ui 0.1.1
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/assets/index.css +10 -0
- package/dist/common-ui.es.js +16404 -0
- package/dist/common-ui.es.js.map +1 -0
- package/dist/common-ui.umd.js +9 -0
- package/dist/common-ui.umd.js.map +1 -0
- package/dist/components/HeatMap.types.d.ts +13 -0
- package/dist/components/PaginatingToolbar.types.d.ts +40 -0
- package/dist/components/SkMouseTrap.types.d.ts +3 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
- package/dist/components/SnackbarService.d.ts +11 -0
- package/dist/components/StudySession.types.d.ts +6 -0
- package/dist/components/auth/index.d.ts +4 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
- package/dist/composables/CompositionViewable.d.ts +33 -0
- package/dist/composables/Displayable.d.ts +47 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/index.d.ts +36 -0
- package/dist/plugins/pinia.d.ts +5 -0
- package/dist/stores/useAuthStore.d.ts +225 -0
- package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
- package/dist/stores/useConfigStore.d.ts +11 -0
- package/dist/utils/SkldrMouseTrap.d.ts +32 -0
- package/package.json +67 -0
- package/src/components/HeatMap.types.ts +15 -0
- package/src/components/HeatMap.vue +354 -0
- package/src/components/PaginatingToolbar.types.ts +48 -0
- package/src/components/PaginatingToolbar.vue +75 -0
- package/src/components/SkMouseTrap.types.ts +3 -0
- package/src/components/SkMouseTrap.vue +70 -0
- package/src/components/SkMouseTrapToolTip.types.ts +41 -0
- package/src/components/SkMouseTrapToolTip.vue +316 -0
- package/src/components/SnackbarService.ts +39 -0
- package/src/components/SnackbarService.vue +71 -0
- package/src/components/StudySession.types.ts +6 -0
- package/src/components/StudySession.vue +670 -0
- package/src/components/StudySessionTimer.vue +121 -0
- package/src/components/auth/UserChip.vue +106 -0
- package/src/components/auth/UserLogin.vue +141 -0
- package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
- package/src/components/auth/UserRegistration.vue +181 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
- package/src/components/cardRendering/CardLoader.vue +123 -0
- package/src/components/cardRendering/CardViewer.vue +101 -0
- package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
- package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
- package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
- package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
- package/src/components/studentInputs/BaseUserInput.ts +71 -0
- package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
- package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
- package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
- package/src/components/studentInputs/TrueFalse.vue +27 -0
- package/src/components/studentInputs/UserInputNumber.vue +63 -0
- package/src/components/studentInputs/UserInputString.vue +89 -0
- package/src/components/studentInputs/fillInInput.vue +71 -0
- package/src/composables/CompositionViewable.ts +180 -0
- package/src/composables/Displayable.ts +133 -0
- package/src/composables/index.ts +2 -0
- package/src/index.ts +79 -0
- package/src/plugins/pinia.ts +24 -0
- package/src/stores/useAuthStore.ts +92 -0
- package/src/stores/useCardPreviewModeStore.ts +32 -0
- package/src/stores/useConfigStore.ts +60 -0
- package/src/utils/SkldrMouseTrap.ts +141 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<svg :width="width" :height="height">
|
|
4
|
+
<g
|
|
5
|
+
v-for="(week, weekIndex) in weeks"
|
|
6
|
+
:key="weekIndex"
|
|
7
|
+
:transform="`translate(${weekIndex * (cellSize + cellMargin)}, 0)`"
|
|
8
|
+
>
|
|
9
|
+
<rect
|
|
10
|
+
v-for="(day, dayIndex) in week"
|
|
11
|
+
:key="day.date"
|
|
12
|
+
:x="0"
|
|
13
|
+
:y="dayIndex * (cellSize + cellMargin)"
|
|
14
|
+
:width="cellSize"
|
|
15
|
+
:height="cellSize"
|
|
16
|
+
:fill="getColor(day.count)"
|
|
17
|
+
@mouseover="showTooltip(day, $event)"
|
|
18
|
+
@mouseout="hideTooltip"
|
|
19
|
+
/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
22
|
+
<div v-if="tooltipData" class="tooltip" :style="tooltipStyle">
|
|
23
|
+
{{ tooltipData.count }} review{{ tooltipData.count !== 1 ? 's' : '' }} on {{ toDateString(tooltipData.date) }}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import { defineComponent, PropType } from 'vue';
|
|
30
|
+
import moment from 'moment';
|
|
31
|
+
import { DayData, Color, ActivityRecord } from './HeatMap.types';
|
|
32
|
+
|
|
33
|
+
export default defineComponent({
|
|
34
|
+
name: 'HeatMap',
|
|
35
|
+
|
|
36
|
+
props: {
|
|
37
|
+
// Accept activity records directly as a prop
|
|
38
|
+
activityRecords: {
|
|
39
|
+
type: Array as PropType<ActivityRecord[]>,
|
|
40
|
+
default: () => [],
|
|
41
|
+
},
|
|
42
|
+
// Accept a function that can retrieve activity records
|
|
43
|
+
activityRecordsGetter: {
|
|
44
|
+
type: Function as PropType<() => Promise<ActivityRecord[]>>,
|
|
45
|
+
default: null,
|
|
46
|
+
},
|
|
47
|
+
// Customize colors
|
|
48
|
+
inactiveColor: {
|
|
49
|
+
type: Object as PropType<Color>,
|
|
50
|
+
default: () => ({ h: 0, s: 0, l: 0.9 }),
|
|
51
|
+
},
|
|
52
|
+
activeColor: {
|
|
53
|
+
type: Object as PropType<Color>,
|
|
54
|
+
default: () => ({ h: 155, s: 1, l: 0.5 }),
|
|
55
|
+
},
|
|
56
|
+
// Customize size
|
|
57
|
+
cellSize: {
|
|
58
|
+
type: Number,
|
|
59
|
+
default: 12,
|
|
60
|
+
},
|
|
61
|
+
cellMargin: {
|
|
62
|
+
type: Number,
|
|
63
|
+
default: 3,
|
|
64
|
+
},
|
|
65
|
+
// Enable/disable seasonal colors
|
|
66
|
+
enableSeasonalColors: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
data() {
|
|
73
|
+
return {
|
|
74
|
+
isLoading: false,
|
|
75
|
+
localActivityRecords: [] as ActivityRecord[],
|
|
76
|
+
heatmapData: {} as { [key: string]: number },
|
|
77
|
+
weeks: [] as DayData[][],
|
|
78
|
+
tooltipData: null as DayData | null,
|
|
79
|
+
tooltipStyle: {} as { [key: string]: string },
|
|
80
|
+
maxInRange: 0,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
computed: {
|
|
85
|
+
width(): number {
|
|
86
|
+
return 53 * (this.cellSize + this.cellMargin);
|
|
87
|
+
},
|
|
88
|
+
height(): number {
|
|
89
|
+
return 7 * (this.cellSize + this.cellMargin);
|
|
90
|
+
},
|
|
91
|
+
effectiveActivityRecords(): ActivityRecord[] {
|
|
92
|
+
const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;
|
|
93
|
+
const records = useLocal ? this.localActivityRecords : this.activityRecords || [];
|
|
94
|
+
console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');
|
|
95
|
+
return records;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
watch: {
|
|
100
|
+
activityRecords: {
|
|
101
|
+
handler() {
|
|
102
|
+
this.processRecords();
|
|
103
|
+
this.createWeeksData();
|
|
104
|
+
},
|
|
105
|
+
immediate: true,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async created() {
|
|
110
|
+
if (this.activityRecordsGetter) {
|
|
111
|
+
try {
|
|
112
|
+
this.isLoading = true;
|
|
113
|
+
console.log('Fetching activity records using getter...');
|
|
114
|
+
|
|
115
|
+
// Ensure the getter is called safely with proper error handling
|
|
116
|
+
let result = await this.activityRecordsGetter();
|
|
117
|
+
|
|
118
|
+
// Handle the result - ensure it's an array of activity records
|
|
119
|
+
if (Array.isArray(result)) {
|
|
120
|
+
// Filter out records with invalid timestamps before processing
|
|
121
|
+
this.localActivityRecords = result.filter(record => {
|
|
122
|
+
if (!record || !record.timeStamp) return false;
|
|
123
|
+
|
|
124
|
+
// Basic validation check for timestamps
|
|
125
|
+
try {
|
|
126
|
+
const m = moment(record.timeStamp);
|
|
127
|
+
return m.isValid() && m.year() > 2000 && m.year() < 2100;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);
|
|
134
|
+
|
|
135
|
+
// Process the loaded records
|
|
136
|
+
this.processRecords();
|
|
137
|
+
this.createWeeksData();
|
|
138
|
+
} else {
|
|
139
|
+
console.error('Activity records getter did not return an array:', result);
|
|
140
|
+
this.localActivityRecords = [];
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Error fetching activity records:', error);
|
|
144
|
+
this.localActivityRecords = [];
|
|
145
|
+
} finally {
|
|
146
|
+
this.isLoading = false;
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
console.log('No activityRecordsGetter provided, using direct activityRecords prop');
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
methods: {
|
|
154
|
+
toDateString(d: string): string {
|
|
155
|
+
const m = moment(d);
|
|
156
|
+
return moment.months()[m.month()] + ' ' + m.date();
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
processRecords() {
|
|
160
|
+
const records = this.effectiveActivityRecords || [];
|
|
161
|
+
console.log(`Processing ${records.length} records`);
|
|
162
|
+
|
|
163
|
+
const data: { [key: string]: number } = {};
|
|
164
|
+
|
|
165
|
+
if (records.length === 0) {
|
|
166
|
+
console.log('No records to process');
|
|
167
|
+
this.heatmapData = data;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sample logging of a few records to understand structure without flooding console
|
|
172
|
+
const uniqueDates = new Set<string>();
|
|
173
|
+
const dateDistribution: Record<string, number> = {};
|
|
174
|
+
let validCount = 0;
|
|
175
|
+
let invalidCount = 0;
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < records.length; i++) {
|
|
178
|
+
const record = records[i];
|
|
179
|
+
|
|
180
|
+
if (!record || typeof record !== 'object' || !record.timeStamp) {
|
|
181
|
+
invalidCount++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Attempt to normalize the timestamp
|
|
187
|
+
let normalizedDate: string;
|
|
188
|
+
|
|
189
|
+
if (typeof record.timeStamp === 'string') {
|
|
190
|
+
// For ISO strings, parse directly with moment
|
|
191
|
+
normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');
|
|
192
|
+
} else if (typeof record.timeStamp === 'number') {
|
|
193
|
+
// For numeric timestamps, use Date constructor then moment
|
|
194
|
+
normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');
|
|
195
|
+
} else if (typeof record.timeStamp === 'object') {
|
|
196
|
+
// For objects (like Moment), try toString() or direct parsing
|
|
197
|
+
if (typeof record.timeStamp.format === 'function') {
|
|
198
|
+
// It's likely a Moment object
|
|
199
|
+
normalizedDate = record.timeStamp.format('YYYY-MM-DD');
|
|
200
|
+
} else if (record.timeStamp instanceof Date) {
|
|
201
|
+
normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');
|
|
202
|
+
} else {
|
|
203
|
+
// Try to parse it as a string representation
|
|
204
|
+
normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Unhandled type
|
|
208
|
+
invalidCount++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Verify the date is valid before using it
|
|
213
|
+
if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {
|
|
214
|
+
data[normalizedDate] = (data[normalizedDate] || 0) + 1;
|
|
215
|
+
uniqueDates.add(normalizedDate);
|
|
216
|
+
|
|
217
|
+
// Track distribution by month for debugging
|
|
218
|
+
const month = normalizedDate.substring(0, 7); // YYYY-MM
|
|
219
|
+
dateDistribution[month] = (dateDistribution[month] || 0) + 1;
|
|
220
|
+
|
|
221
|
+
validCount++;
|
|
222
|
+
} else {
|
|
223
|
+
invalidCount++;
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
invalidCount++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Log summary statistics
|
|
231
|
+
console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);
|
|
232
|
+
console.log(`Found ${uniqueDates.size} unique dates`);
|
|
233
|
+
console.log('Date distribution by month:', dateDistribution);
|
|
234
|
+
|
|
235
|
+
this.heatmapData = data;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
createWeeksData() {
|
|
239
|
+
// Reset weeks and max count
|
|
240
|
+
this.weeks = [];
|
|
241
|
+
this.maxInRange = 0;
|
|
242
|
+
|
|
243
|
+
const end = moment();
|
|
244
|
+
const start = end.clone().subtract(52, 'weeks');
|
|
245
|
+
const day = start.clone().startOf('week');
|
|
246
|
+
|
|
247
|
+
console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));
|
|
248
|
+
|
|
249
|
+
// Ensure we have data to display
|
|
250
|
+
if (Object.keys(this.heatmapData).length === 0) {
|
|
251
|
+
console.log('No heatmap data available to display');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// For debugging, log some sample dates from the heatmap data
|
|
255
|
+
const sampleDates = Object.keys(this.heatmapData).slice(0, 5);
|
|
256
|
+
console.log('Sample dates in heatmap data:', sampleDates);
|
|
257
|
+
|
|
258
|
+
// Build the week data structure
|
|
259
|
+
while (day.isSameOrBefore(end)) {
|
|
260
|
+
const weekData: DayData[] = [];
|
|
261
|
+
for (let i = 0; i < 7; i++) {
|
|
262
|
+
const date = day.format('YYYY-MM-DD');
|
|
263
|
+
const count = this.heatmapData[date] || 0;
|
|
264
|
+
const dayData: DayData = {
|
|
265
|
+
date,
|
|
266
|
+
count,
|
|
267
|
+
};
|
|
268
|
+
weekData.push(dayData);
|
|
269
|
+
if (dayData.count > this.maxInRange) {
|
|
270
|
+
this.maxInRange = dayData.count;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
day.add(1, 'day');
|
|
274
|
+
}
|
|
275
|
+
this.weeks.push(weekData);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log('Weeks data created, maxInRange:', this.maxInRange);
|
|
279
|
+
|
|
280
|
+
// Calculate summary stats for display
|
|
281
|
+
let totalDaysWithActivity = 0;
|
|
282
|
+
let totalActivity = 0;
|
|
283
|
+
|
|
284
|
+
Object.values(this.heatmapData).forEach(count => {
|
|
285
|
+
totalDaysWithActivity++;
|
|
286
|
+
totalActivity += count;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
getColor(count: number): string {
|
|
293
|
+
if (this.maxInRange === 0) return this.hslToString(this.inactiveColor);
|
|
294
|
+
|
|
295
|
+
const t = count === 0 ? 0 : Math.min((2 * count) / this.maxInRange, 1);
|
|
296
|
+
|
|
297
|
+
let seasonalColor: Color = this.activeColor;
|
|
298
|
+
|
|
299
|
+
if (this.enableSeasonalColors) {
|
|
300
|
+
const now = moment();
|
|
301
|
+
if (now.month() === 11 && now.date() >= 5) {
|
|
302
|
+
// Christmas colors
|
|
303
|
+
seasonalColor = Math.random() > 0.5 ? { h: 350, s: 0.8, l: 0.5 } : { h: 135, s: 0.8, l: 0.4 };
|
|
304
|
+
} else if (now.month() === 9 && now.date() >= 25) {
|
|
305
|
+
// Halloween colors
|
|
306
|
+
seasonalColor =
|
|
307
|
+
Math.random() > 0.5
|
|
308
|
+
? { h: 0, s: 0, l: 0 }
|
|
309
|
+
: Math.random() > 0.5
|
|
310
|
+
? { h: 30, s: 1, l: 0.5 }
|
|
311
|
+
: { h: 270, s: 1, l: 0.5 };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const h = seasonalColor.h;
|
|
316
|
+
const s = this.interpolate(this.inactiveColor.s, seasonalColor.s, t);
|
|
317
|
+
const l = this.interpolate(this.inactiveColor.l, seasonalColor.l, t);
|
|
318
|
+
|
|
319
|
+
return this.hslToString({ h, s, l });
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
interpolate(start: number, end: number, t: number): number {
|
|
323
|
+
return start + (end - start) * t;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
hslToString(color: Color): string {
|
|
327
|
+
return `hsl(${color.h}, ${color.s * 100}%, ${color.l * 100}%)`;
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
showTooltip(day: DayData, event: MouseEvent) {
|
|
331
|
+
this.tooltipData = day;
|
|
332
|
+
this.tooltipStyle = {
|
|
333
|
+
position: 'absolute',
|
|
334
|
+
left: `${event.pageX + 10}px`,
|
|
335
|
+
top: `${event.pageY + 10}px`,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
hideTooltip() {
|
|
340
|
+
this.tooltipData = null;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
<style scoped>
|
|
347
|
+
.tooltip {
|
|
348
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
349
|
+
color: white;
|
|
350
|
+
padding: 5px;
|
|
351
|
+
border-radius: 3px;
|
|
352
|
+
font-size: 12px;
|
|
353
|
+
}
|
|
354
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface PaginatingToolbarProps {
|
|
2
|
+
/**
|
|
3
|
+
* Array of page numbers
|
|
4
|
+
*/
|
|
5
|
+
pages: number[];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Current active page
|
|
9
|
+
*/
|
|
10
|
+
page: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main title displayed in the toolbar
|
|
14
|
+
*/
|
|
15
|
+
title?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Secondary text displayed next to the title
|
|
19
|
+
*/
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PaginatingToolbarEvents {
|
|
24
|
+
/**
|
|
25
|
+
* Navigate to the first page
|
|
26
|
+
*/
|
|
27
|
+
(e: 'first'): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Navigate to the previous page
|
|
31
|
+
*/
|
|
32
|
+
(e: 'prev'): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Navigate to the next page
|
|
36
|
+
*/
|
|
37
|
+
(e: 'next'): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Navigate to the last page
|
|
41
|
+
*/
|
|
42
|
+
(e: 'last'): void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set the page to a specific number
|
|
46
|
+
*/
|
|
47
|
+
(e: 'set-page', page: number): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-toolbar density="compact">
|
|
3
|
+
<v-toolbar-title>
|
|
4
|
+
<span>{{ title }}</span>
|
|
5
|
+
<span v-if="subtitle" class="ms-2 text-subtitle-2">{{ subtitle }}</span>
|
|
6
|
+
</v-toolbar-title>
|
|
7
|
+
<v-spacer></v-spacer>
|
|
8
|
+
<v-btn variant="text" icon color="secondary" :disabled="page == 1" @click="$emit('first')">
|
|
9
|
+
<v-icon>mdi-page-first</v-icon>
|
|
10
|
+
</v-btn>
|
|
11
|
+
<v-btn variant="text" icon color="secondary" :disabled="page == 1" @click="$emit('prev')">
|
|
12
|
+
<v-icon>mdi-chevron-left</v-icon>
|
|
13
|
+
</v-btn>
|
|
14
|
+
|
|
15
|
+
<v-select
|
|
16
|
+
:model-value="page"
|
|
17
|
+
:items="pages"
|
|
18
|
+
class="pageSelect"
|
|
19
|
+
density="compact"
|
|
20
|
+
hide-details
|
|
21
|
+
:return-object="false"
|
|
22
|
+
variant="outlined"
|
|
23
|
+
@update:model-value="(val: unknown) => $emit('set-page', val)"
|
|
24
|
+
>
|
|
25
|
+
<template #selection="{ item }">
|
|
26
|
+
{{ item.value }}
|
|
27
|
+
</template>
|
|
28
|
+
</v-select>
|
|
29
|
+
|
|
30
|
+
<v-btn variant="text" icon color="secondary" :disabled="page == pages.length" @click="$emit('next')">
|
|
31
|
+
<v-icon>mdi-chevron-right</v-icon>
|
|
32
|
+
</v-btn>
|
|
33
|
+
<v-btn variant="text" icon color="secondary" :disabled="page == pages.length" @click="$emit('last')">
|
|
34
|
+
<v-icon>mdi-page-last</v-icon>
|
|
35
|
+
</v-btn>
|
|
36
|
+
</v-toolbar>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { defineComponent, PropType } from 'vue';
|
|
41
|
+
import type { PaginatingToolbarProps, PaginatingToolbarEvents } from './PaginatingToolbar.types';
|
|
42
|
+
|
|
43
|
+
export default defineComponent({
|
|
44
|
+
name: 'PaginatingToolbar',
|
|
45
|
+
|
|
46
|
+
props: {
|
|
47
|
+
pages: {
|
|
48
|
+
type: Array as PropType<number[]>,
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
page: {
|
|
52
|
+
type: Number,
|
|
53
|
+
required: true,
|
|
54
|
+
},
|
|
55
|
+
title: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: false,
|
|
58
|
+
default: '',
|
|
59
|
+
},
|
|
60
|
+
subtitle: {
|
|
61
|
+
type: String,
|
|
62
|
+
required: false,
|
|
63
|
+
default: '',
|
|
64
|
+
},
|
|
65
|
+
} as const,
|
|
66
|
+
|
|
67
|
+
emits: ['first', 'prev', 'next', 'last', 'set-page'],
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
.pageSelect {
|
|
73
|
+
max-width: 60px;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog v-if="display" max-width="500px" transition="dialog-transition">
|
|
3
|
+
<template #activator="{ props }">
|
|
4
|
+
<v-btn icon color="primary" v-bind="props">
|
|
5
|
+
<v-icon>mdi-keyboard</v-icon>
|
|
6
|
+
</v-btn>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<v-card>
|
|
10
|
+
<v-toolbar color="teal" dark dense>
|
|
11
|
+
<v-toolbar-title class="text-subtitle-1">Shortcut keys:</v-toolbar-title>
|
|
12
|
+
</v-toolbar>
|
|
13
|
+
<v-list dense>
|
|
14
|
+
<v-list-item
|
|
15
|
+
v-for="hk in commands"
|
|
16
|
+
:key="Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey"
|
|
17
|
+
class="py-1"
|
|
18
|
+
>
|
|
19
|
+
<v-btn variant="outlined" color="primary" class="text-white" size="small">
|
|
20
|
+
{{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}
|
|
21
|
+
</v-btn>
|
|
22
|
+
<v-spacer></v-spacer>
|
|
23
|
+
<span class="text-caption ml-2">{{ hk.command }}</span>
|
|
24
|
+
</v-list-item>
|
|
25
|
+
</v-list>
|
|
26
|
+
</v-card>
|
|
27
|
+
</v-dialog>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { defineComponent, PropType } from 'vue';
|
|
32
|
+
import { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';
|
|
33
|
+
import { SkMouseTrapProps } from './SkMouseTrap.types';
|
|
34
|
+
|
|
35
|
+
export default defineComponent({
|
|
36
|
+
name: 'SkMouseTrap',
|
|
37
|
+
|
|
38
|
+
props: {
|
|
39
|
+
refreshInterval: {
|
|
40
|
+
type: Number as PropType<SkMouseTrapProps['refreshInterval']>,
|
|
41
|
+
default: 500,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
data() {
|
|
46
|
+
return {
|
|
47
|
+
commands: [] as HotKeyMetaData[],
|
|
48
|
+
display: false,
|
|
49
|
+
intervalId: null as number | null,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
created() {
|
|
54
|
+
this.intervalId = window.setInterval(this.refreshState, this.refreshInterval);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
beforeUnmount() {
|
|
58
|
+
if (this.intervalId !== null) {
|
|
59
|
+
clearInterval(this.intervalId);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
methods: {
|
|
64
|
+
refreshState() {
|
|
65
|
+
this.commands = SkldrMouseTrap.commands;
|
|
66
|
+
this.display = this.commands.length > 0;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SkMouseTrapToolTipProps {
|
|
2
|
+
/**
|
|
3
|
+
* The keyboard shortcut(s) to bind to this tooltip.
|
|
4
|
+
* Can be a single string like "ctrl+s" or an array of strings.
|
|
5
|
+
*/
|
|
6
|
+
hotkey: string | string[];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Descriptive name for the keyboard shortcut.
|
|
10
|
+
* Will be displayed in the shortcut dialog.
|
|
11
|
+
*/
|
|
12
|
+
command: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the shortcut is currently disabled.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The position of the tooltip relative to the wrapped element.
|
|
22
|
+
* @default 'top'
|
|
23
|
+
*/
|
|
24
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to show the tooltip when the Ctrl key is pressed.
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
showTooltip?: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The visual effect to apply to the wrapped element when Ctrl is pressed.
|
|
34
|
+
* @default 'glow'
|
|
35
|
+
*/
|
|
36
|
+
highlightEffect?: 'glow' | 'scale' | 'border' | 'none';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SkMouseTrapToolTipEmits {
|
|
40
|
+
(event: 'hotkey-triggered', hotkey: string | string[]): void;
|
|
41
|
+
}
|