@caido-utils/components 0.3.1 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  type __VLS_Props = {
2
- modelValue: unknown[];
2
+ modelValue: unknown[] | undefined;
3
3
  options: unknown[];
4
4
  optionLabel?: string;
5
5
  optionValue?: string;
@@ -4,7 +4,7 @@ import { computed } from "vue";
4
4
 
5
5
  const props = withDefaults(
6
6
  defineProps<{
7
- modelValue: unknown[];
7
+ modelValue: unknown[] | undefined;
8
8
  options: unknown[];
9
9
  optionLabel?: string;
10
10
  optionValue?: string;
@@ -1,5 +1,5 @@
1
1
  type __VLS_Props = {
2
- modelValue: unknown[];
2
+ modelValue: unknown[] | undefined;
3
3
  options: unknown[];
4
4
  optionLabel?: string;
5
5
  optionValue?: string;
package/dist/index.d.ts CHANGED
@@ -9,4 +9,3 @@ export { default as MenuButton } from "./MenuButton/Container.vue";
9
9
  export { default as MultiSelect } from "./MultiSelect/Container.vue";
10
10
  export { default as RequestEditor } from "./RequestEditor/Container.vue";
11
11
  export { default as ResponseEditor } from "./ResponseEditor/Container.vue";
12
- export { default as Table } from "./Table/Container.vue";
package/dist/index.js CHANGED
@@ -9,4 +9,3 @@ export { default as MenuButton } from "./MenuButton/Container.vue";
9
9
  export { default as MultiSelect } from "./MultiSelect/Container.vue";
10
10
  export { default as RequestEditor } from "./RequestEditor/Container.vue";
11
11
  export { default as ResponseEditor } from "./ResponseEditor/Container.vue";
12
- export { default as Table } from "./Table/Container.vue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caido-utils/components",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -1,36 +0,0 @@
1
- import type { MenuItem } from "primevue/menuitem";
2
- declare const _default: <T extends Record<string, unknown>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
- props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & Readonly<import("vue").ExtractPropTypes<{}>>, never>, never> & {
4
- items: T[];
5
- selected: T[];
6
- columns: {
7
- key: string;
8
- header: string;
9
- width: string;
10
- sortable?: boolean;
11
- sortType?: "string" | "number" | "date";
12
- }[];
13
- rowKey: (item: T) => string;
14
- activeKey?: string;
15
- emptyMessage?: string;
16
- selectable?: boolean;
17
- scrollKey?: string;
18
- contextMenu?: MenuItem[];
19
- resizable?: boolean;
20
- } & Partial<{}>> & import("vue").PublicProps;
21
- expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
22
- attrs: any;
23
- slots: {
24
- [x: string]: ((props: {
25
- item: any;
26
- value: any;
27
- }) => any) | undefined;
28
- };
29
- emit: ((evt: "row-click", item: T) => void) & ((evt: "row-contextmenu", item: T, event: MouseEvent) => void) & ((evt: "update:selected", value: T[]) => void);
30
- }>) => import("vue").VNode & {
31
- __ctx?: Awaited<typeof __VLS_setup>;
32
- };
33
- export default _default;
34
- type __VLS_PrettifyLocal<T> = {
35
- [K in keyof T]: T[K];
36
- } & {};
@@ -1,512 +0,0 @@
1
- <script lang="ts">
2
- const scrollMemory = new Map<string, number>();
3
- </script>
4
-
5
- <script setup lang="ts" generic="T extends Record<string, unknown>">
6
- import ContextMenu from "primevue/contextmenu";
7
- import type { MenuItem } from "primevue/menuitem";
8
- import VirtualScroller from "primevue/virtualscroller";
9
- import {
10
- computed,
11
- nextTick,
12
- onBeforeUnmount,
13
- onMounted,
14
- ref,
15
- shallowRef,
16
- triggerRef,
17
- watch,
18
- } from "vue";
19
-
20
- type SortType = "string" | "number" | "date";
21
-
22
- type Column = {
23
- key: string;
24
- header: string;
25
- width: string;
26
- sortable?: boolean;
27
- sortType?: SortType;
28
- };
29
-
30
- type SortOrder = "asc" | "desc" | undefined;
31
-
32
- const props = withDefaults(
33
- defineProps<{
34
- items: T[];
35
- selected: T[];
36
- columns: Column[];
37
- rowKey: (item: T) => string;
38
- activeKey?: string;
39
- emptyMessage?: string;
40
- selectable?: boolean;
41
- scrollKey?: string;
42
- contextMenu?: MenuItem[];
43
- resizable?: boolean;
44
- }>(),
45
- { selectable: false, resizable: false },
46
- );
47
-
48
- const emit = defineEmits<{
49
- "update:selected": [value: T[]];
50
- "row-click": [item: T];
51
- "row-contextmenu": [item: T, event: MouseEvent];
52
- }>();
53
-
54
- const contextMenuRef = ref();
55
-
56
- function onRowContextMenu(item: T, event: MouseEvent) {
57
- event.preventDefault();
58
- emit("row-contextmenu", item, event);
59
- if (props.contextMenu && contextMenuRef.value) {
60
- contextMenuRef.value.show(event);
61
- }
62
- }
63
-
64
- const selectedKeys = ref(new Set<string>());
65
-
66
- watch(
67
- () => props.selected,
68
- (val) => {
69
- const keys = new Set<string>();
70
- for (const item of val) {
71
- keys.add(props.rowKey(item));
72
- }
73
- selectedKeys.value = keys;
74
- },
75
- { immediate: true },
76
- );
77
-
78
- function syncSelection() {
79
- const result: T[] = [];
80
- for (const item of props.items) {
81
- if (selectedKeys.value.has(props.rowKey(item))) {
82
- result.push(item);
83
- }
84
- }
85
- emit("update:selected", result);
86
- }
87
-
88
- function toggleSelection(item: T) {
89
- const key = props.rowKey(item);
90
- if (selectedKeys.value.has(key)) {
91
- selectedKeys.value.delete(key);
92
- } else {
93
- selectedKeys.value.add(key);
94
- }
95
- syncSelection();
96
- }
97
-
98
- function toggleAll() {
99
- if (selectedKeys.value.size === props.items.length) {
100
- selectedKeys.value.clear();
101
- } else {
102
- for (const item of props.items) {
103
- selectedKeys.value.add(props.rowKey(item));
104
- }
105
- }
106
- syncSelection();
107
- }
108
-
109
- const allSelected = computed(
110
- () =>
111
- props.items.length > 0 && selectedKeys.value.size === props.items.length,
112
- );
113
-
114
- function onRowClick(item: T) {
115
- emit("row-click", item);
116
- }
117
-
118
- function isSelected(item: T): boolean {
119
- return selectedKeys.value.has(props.rowKey(item));
120
- }
121
-
122
- function isActive(item: T): boolean {
123
- if (props.activeKey === undefined) return false;
124
- return props.rowKey(item) === props.activeKey;
125
- }
126
-
127
- const sortKey = ref<string | undefined>(undefined);
128
- const sortOrder = ref<SortOrder>(undefined);
129
-
130
- function toggleSort(col: Column) {
131
- if (!col.sortable) return;
132
-
133
- if (sortKey.value !== col.key) {
134
- sortKey.value = col.key;
135
- sortOrder.value = "asc";
136
- } else if (sortOrder.value === "asc") {
137
- sortOrder.value = "desc";
138
- } else {
139
- sortKey.value = undefined;
140
- sortOrder.value = undefined;
141
- }
142
- }
143
-
144
- const sortedItems = computed(() => {
145
- if (sortKey.value === undefined || sortOrder.value === undefined) {
146
- return props.items;
147
- }
148
-
149
- const key = sortKey.value;
150
- const col = props.columns.find((c) => c.key === key);
151
- const type = col?.sortType ?? "string";
152
- const dir = sortOrder.value === "asc" ? 1 : -1;
153
- const sorted = [...props.items];
154
-
155
- sorted.sort((a, b) => {
156
- const aVal = a[key];
157
- const bVal = b[key];
158
-
159
- if (aVal === undefined || aVal === "") return 1;
160
- if (bVal === undefined || bVal === "") return -1;
161
-
162
- if (type === "number") {
163
- return (Number(aVal) - Number(bVal)) * dir;
164
- }
165
-
166
- if (type === "date") {
167
- return (
168
- (new Date(String(aVal)).getTime() - new Date(String(bVal)).getTime()) *
169
- dir
170
- );
171
- }
172
-
173
- return String(aVal).localeCompare(String(bVal)) * dir;
174
- });
175
-
176
- return sorted;
177
- });
178
-
179
- const VIRTUAL_THRESHOLD = 100;
180
- const useVirtual = computed(
181
- () => sortedItems.value.length >= VIRTUAL_THRESHOLD,
182
- );
183
-
184
- const scrollerKey = ref(0);
185
- const containerRef = ref<HTMLElement>();
186
- let resizeObserver: ResizeObserver | undefined;
187
-
188
- function getScrollElement(): HTMLElement | undefined {
189
- if (!containerRef.value) return undefined;
190
- if (useVirtual.value) {
191
- return (
192
- containerRef.value.querySelector<HTMLElement>(".p-virtualscroller") ??
193
- undefined
194
- );
195
- }
196
- return containerRef.value;
197
- }
198
-
199
- function onScroll(e: Event) {
200
- if (!props.scrollKey) return;
201
- const target = e.target as HTMLElement;
202
- scrollMemory.set(props.scrollKey, target.scrollTop);
203
- }
204
-
205
- function attachScrollListener() {
206
- const el = getScrollElement();
207
- el?.addEventListener("scroll", onScroll, { passive: true });
208
- }
209
-
210
- function detachScrollListener() {
211
- const el = getScrollElement();
212
- el?.removeEventListener("scroll", onScroll);
213
- }
214
-
215
- function restoreScrollPosition() {
216
- if (!props.scrollKey) return;
217
- const saved = scrollMemory.get(props.scrollKey);
218
- if (saved === undefined || saved <= 0) return;
219
- const el = getScrollElement();
220
- if (el) {
221
- el.scrollTop = saved;
222
- }
223
- }
224
-
225
- // Resizable columns
226
- const headerTableRef = ref<HTMLTableElement>();
227
- const columnWidths = shallowRef<number[]>([]);
228
- const widthsInitialized = ref(false);
229
- const resizing = ref(false);
230
- const resizeColIndex = ref(-1);
231
- const resizeStartX = ref(0);
232
- const resizeStartWidth = ref(0);
233
-
234
- const colStyles = computed(() =>
235
- props.columns.map((col, i) => {
236
- if (
237
- props.resizable &&
238
- widthsInitialized.value &&
239
- columnWidths.value[i] !== undefined
240
- ) {
241
- return { width: `${columnWidths.value[i]}px` };
242
- }
243
- return { width: col.width };
244
- }),
245
- );
246
-
247
- function initColumnWidths() {
248
- if (!props.resizable || !headerTableRef.value) return;
249
- const tableWidth = headerTableRef.value.offsetWidth;
250
- if (tableWidth === 0) return;
251
- const ths = headerTableRef.value.querySelectorAll("thead th");
252
- const startIdx = props.selectable ? 1 : 0;
253
- const rawWidths: number[] = [];
254
- let fixedTotal = 0;
255
- let zeroCount = 0;
256
- for (let i = startIdx; i < ths.length; i++) {
257
- const w = (ths[i] as HTMLElement).offsetWidth;
258
- rawWidths.push(w);
259
- if (w === 0) {
260
- zeroCount++;
261
- } else {
262
- fixedTotal += w;
263
- }
264
- }
265
- const checkboxWidth =
266
- props.selectable && startIdx === 1
267
- ? (ths[0] as HTMLElement).offsetWidth
268
- : 0;
269
- const remaining = tableWidth - checkboxWidth - fixedTotal;
270
- const flexWidth =
271
- zeroCount > 0 ? Math.max(30, Math.floor(remaining / zeroCount)) : 0;
272
- const widths = rawWidths.map((w) => (w === 0 ? flexWidth : w));
273
- columnWidths.value = widths;
274
- widthsInitialized.value = true;
275
- }
276
-
277
- function onResizeStart(colIndex: number, event: MouseEvent) {
278
- resizing.value = true;
279
- resizeColIndex.value = colIndex;
280
- resizeStartX.value = event.clientX;
281
- resizeStartWidth.value = columnWidths.value[colIndex] ?? 0;
282
- document.addEventListener("mousemove", onResizeMove);
283
- document.addEventListener("mouseup", onResizeEnd);
284
- document.body.style.cursor = "col-resize";
285
- document.body.style.userSelect = "none";
286
- }
287
-
288
- function onResizeMove(event: MouseEvent) {
289
- if (!resizing.value) return;
290
- const delta = event.clientX - resizeStartX.value;
291
- const newWidth = Math.max(30, resizeStartWidth.value + delta);
292
- columnWidths.value[resizeColIndex.value] = newWidth;
293
- triggerRef(columnWidths);
294
- }
295
-
296
- function onResizeEnd() {
297
- resizing.value = false;
298
- resizeColIndex.value = -1;
299
- document.removeEventListener("mousemove", onResizeMove);
300
- document.removeEventListener("mouseup", onResizeEnd);
301
- document.body.style.cursor = "";
302
- document.body.style.userSelect = "";
303
- }
304
-
305
- watch(
306
- () => props.columns.map((c) => c.key).join(","),
307
- async () => {
308
- if (!props.resizable) return;
309
- widthsInitialized.value = false;
310
- await nextTick();
311
- initColumnWidths();
312
- },
313
- );
314
-
315
- watch(scrollerKey, async () => {
316
- await nextTick();
317
- attachScrollListener();
318
- restoreScrollPosition();
319
- });
320
-
321
- onMounted(() => {
322
- if (containerRef.value === undefined) return;
323
- resizeObserver = new ResizeObserver((entries) => {
324
- const entry = entries[0];
325
- if (entry !== undefined && entry.contentRect.height > 0) {
326
- scrollerKey.value++;
327
- }
328
- });
329
- resizeObserver.observe(containerRef.value);
330
-
331
- if (!useVirtual.value) {
332
- nextTick(() => {
333
- attachScrollListener();
334
- restoreScrollPosition();
335
- });
336
- }
337
-
338
- nextTick(() => initColumnWidths());
339
- });
340
-
341
- onBeforeUnmount(() => {
342
- detachScrollListener();
343
- resizeObserver?.disconnect();
344
- if (resizing.value) {
345
- onResizeEnd();
346
- }
347
- });
348
- </script>
349
-
350
- <template>
351
- <ContextMenu v-if="contextMenu" ref="contextMenuRef" :model="contextMenu" />
352
- <div class="h-full flex flex-col min-h-0">
353
- <table
354
- ref="headerTableRef"
355
- class="w-full border-collapse"
356
- :style="{ tableLayout: 'fixed' }"
357
- >
358
- <colgroup>
359
- <col v-if="selectable" style="width: 32px" />
360
- <col v-for="(col, i) in columns" :key="col.key" :style="colStyles[i]" />
361
- </colgroup>
362
- <thead>
363
- <tr>
364
- <th
365
- v-if="selectable"
366
- class="px-2 py-1.5 bg-surface-900 sticky top-0 z-10"
367
- >
368
- <div
369
- class="w-3.5 h-3.5 rounded-sm border cursor-pointer flex items-center justify-center transition-colors"
370
- :class="
371
- allSelected
372
- ? 'bg-amber-500 border-amber-500'
373
- : 'border-surface-500 hover:border-surface-300'
374
- "
375
- @click="toggleAll()"
376
- >
377
- <i
378
- v-if="allSelected"
379
- class="fas fa-check text-[8px] text-white"
380
- />
381
- </div>
382
- </th>
383
- <th
384
- v-for="(col, i) in columns"
385
- :key="col.key"
386
- class="px-2.5 py-1.5 font-mono text-sm font-semibold text-surface-300 text-left bg-surface-900 border-b border-surface-700/50 sticky top-0 z-10 select-none"
387
- :class="resizable ? 'relative' : ''"
388
- >
389
- <span
390
- class="flex items-center gap-1.5"
391
- :class="
392
- col.sortable
393
- ? 'cursor-pointer hover:text-surface-100 transition-colors'
394
- : ''
395
- "
396
- @click="toggleSort(col)"
397
- >
398
- {{ col.header }}
399
- <i
400
- v-if="col.sortable && sortKey === col.key"
401
- :class="[
402
- 'fas text-[9px]',
403
- sortOrder === 'asc' ? 'fa-chevron-up' : 'fa-chevron-down',
404
- ]"
405
- />
406
- </span>
407
- <div
408
- v-if="resizable"
409
- class="absolute top-0 -right-px w-1.5 h-full cursor-col-resize bg-surface-700 hover:bg-amber-500 z-20"
410
- @mousedown.prevent.stop="onResizeStart(i, $event)"
411
- />
412
- </th>
413
- </tr>
414
- </thead>
415
- </table>
416
- <div
417
- ref="containerRef"
418
- class="flex-1 min-h-0"
419
- :class="useVirtual ? 'overflow-hidden' : 'overflow-y-auto'"
420
- >
421
- <div
422
- v-if="sortedItems.length === 0"
423
- class="flex flex-col items-center justify-center h-full gap-3 text-surface-500"
424
- >
425
- <i class="fas fa-inbox text-3xl text-surface-600" />
426
- <span class="text-sm">{{ emptyMessage ?? "No data" }}</span>
427
- </div>
428
- <VirtualScroller
429
- v-else
430
- :key="scrollerKey"
431
- :items="sortedItems"
432
- :item-size="33"
433
- :disabled="!useVirtual"
434
- class="h-full"
435
- >
436
- <template
437
- #content="{
438
- items: visibleItems,
439
- styleClass,
440
- contentRef,
441
- contentStyle,
442
- }"
443
- >
444
- <table
445
- :ref="contentRef"
446
- :class="styleClass"
447
- class="w-full border-collapse"
448
- :style="{ tableLayout: 'fixed', ...contentStyle }"
449
- >
450
- <colgroup>
451
- <col v-if="selectable" style="width: 32px" />
452
- <col
453
- v-for="(col, i) in columns"
454
- :key="col.key"
455
- :style="colStyles[i]"
456
- />
457
- </colgroup>
458
- <tbody>
459
- <tr
460
- v-for="item in visibleItems as T[]"
461
- :key="rowKey(item)"
462
- class="cursor-pointer"
463
- :class="[
464
- isActive(item)
465
- ? 'bg-amber-400/15 hover:bg-amber-400/25'
466
- : isSelected(item)
467
- ? 'bg-amber-400/10 hover:bg-amber-400/15'
468
- : 'even:bg-white/5 hover:bg-white/10',
469
- ]"
470
- @click="onRowClick(item)"
471
- @contextmenu="onRowContextMenu(item, $event)"
472
- >
473
- <td
474
- v-if="selectable"
475
- class="px-2 py-1.5 border-b border-surface-700/50"
476
- @click.stop="toggleSelection(item)"
477
- >
478
- <div
479
- class="w-3.5 h-3.5 rounded-sm border cursor-pointer flex items-center justify-center transition-colors"
480
- :class="
481
- isSelected(item)
482
- ? 'bg-amber-500 border-amber-500'
483
- : 'border-surface-600 hover:border-surface-400'
484
- "
485
- >
486
- <i
487
- v-if="isSelected(item)"
488
- class="fas fa-check text-[8px] text-white"
489
- />
490
- </div>
491
- </td>
492
- <td
493
- v-for="col in columns"
494
- :key="col.key"
495
- class="px-2.5 py-1.5 font-mono text-[13px] font-medium text-surface-200 overflow-hidden text-ellipsis whitespace-nowrap border-b border-surface-700/50"
496
- >
497
- <slot
498
- :name="`cell-${col.key}`"
499
- :item="item"
500
- :value="item[col.key]"
501
- >
502
- <span class="truncate block">{{ item[col.key] }}</span>
503
- </slot>
504
- </td>
505
- </tr>
506
- </tbody>
507
- </table>
508
- </template>
509
- </VirtualScroller>
510
- </div>
511
- </div>
512
- </template>
@@ -1,36 +0,0 @@
1
- import type { MenuItem } from "primevue/menuitem";
2
- declare const _default: <T extends Record<string, unknown>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
- props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & Readonly<import("vue").ExtractPropTypes<{}>>, never>, never> & {
4
- items: T[];
5
- selected: T[];
6
- columns: {
7
- key: string;
8
- header: string;
9
- width: string;
10
- sortable?: boolean;
11
- sortType?: "string" | "number" | "date";
12
- }[];
13
- rowKey: (item: T) => string;
14
- activeKey?: string;
15
- emptyMessage?: string;
16
- selectable?: boolean;
17
- scrollKey?: string;
18
- contextMenu?: MenuItem[];
19
- resizable?: boolean;
20
- } & Partial<{}>> & import("vue").PublicProps;
21
- expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
22
- attrs: any;
23
- slots: {
24
- [x: string]: ((props: {
25
- item: any;
26
- value: any;
27
- }) => any) | undefined;
28
- };
29
- emit: ((evt: "row-click", item: T) => void) & ((evt: "row-contextmenu", item: T, event: MouseEvent) => void) & ((evt: "update:selected", value: T[]) => void);
30
- }>) => import("vue").VNode & {
31
- __ctx?: Awaited<typeof __VLS_setup>;
32
- };
33
- export default _default;
34
- type __VLS_PrettifyLocal<T> = {
35
- [K in keyof T]: T[K];
36
- } & {};