@txstate-mws/svelte-components 1.6.16 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Displays arbitrary JSON-ish data as a compact nested structure: property lists
|
|
4
|
+
for objects (and Maps), bulleted lists for arrays (and Sets), scalars as text.
|
|
5
|
+
Intended for read-only summaries like preview panes, not for editing.
|
|
6
|
+
|
|
7
|
+
Notes on odd input:
|
|
8
|
+
- datetimes render as ISO8601 in the browser's timezone, whether they arrive as
|
|
9
|
+
Date objects or ISO strings (e.g. UTC from a JSON API); use `format` to display
|
|
10
|
+
them some other way
|
|
11
|
+
- a Map renders like an object as long as every key is a string or has a custom
|
|
12
|
+
toString; otherwise the Map is skipped entirely
|
|
13
|
+
- cyclic structures are safe: a container that appears among its own ancestors
|
|
14
|
+
renders the placeholder at the point of the cycle
|
|
15
|
+
- empty arrays/objects, functions, and null/undefined render nothing
|
|
16
|
+
-->
|
|
17
|
+
<script lang="ts" context="module">
|
|
18
|
+
import { dateToISOWithTZ } from 'txstate-utils'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* datetimes display as ISO8601 in the browser's timezone, whether they arrive
|
|
22
|
+
* as Date objects or ISO strings (e.g. UTC from a JSON API); date-only strings
|
|
23
|
+
* carry no timezone semantics and pass through untouched
|
|
24
|
+
*/
|
|
25
|
+
const isoDatetimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/
|
|
26
|
+
|
|
27
|
+
function localISO (dt: Date) {
|
|
28
|
+
return dateToISOWithTZ(dt).replace('.000', '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderScalar (value: any) {
|
|
32
|
+
if (value instanceof Date) return localISO(value)
|
|
33
|
+
if (typeof value === 'string' && isoDatetimePattern.test(value)) {
|
|
34
|
+
const parsed = new Date(value)
|
|
35
|
+
if (!isNaN(parsed.getTime())) return localISO(parsed)
|
|
36
|
+
}
|
|
37
|
+
return String(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** a Map key we can display: a primitive or anything with a custom toString */
|
|
41
|
+
function stringableKey (key: any) {
|
|
42
|
+
if (key == null) return false
|
|
43
|
+
if (typeof key === 'object' || typeof key === 'function') return key.toString !== Object.prototype.toString
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function arrayEntries (value: any): any[] | undefined {
|
|
48
|
+
if (Array.isArray(value)) return value
|
|
49
|
+
if (value instanceof Set) return Array.from(value)
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function objectEntries (value: any): [string, any][] | undefined {
|
|
54
|
+
if (value instanceof Map) {
|
|
55
|
+
const entries = Array.from(value.entries())
|
|
56
|
+
if (entries.some(([k]) => !stringableKey(k))) return undefined
|
|
57
|
+
return entries.map(([k, v]) => [String(k), v] as [string, any])
|
|
58
|
+
}
|
|
59
|
+
if (value != null && typeof value === 'object' && !(value instanceof Date)) return Object.entries(value)
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<script lang="ts">
|
|
65
|
+
export let data: any
|
|
66
|
+
/** where this value sits in the root data, e.g. ['sections', 0, 'heading']; the root is [] */
|
|
67
|
+
export let path: (string | number)[] = []
|
|
68
|
+
/**
|
|
69
|
+
* how many nested arrays/objects deep we render before eliding with a
|
|
70
|
+
* placeholder
|
|
71
|
+
*
|
|
72
|
+
* e.g. with the default of 5, { items: [{ moreitems: [{ id: 5 }] }] } displays
|
|
73
|
+
* fully - the root object is level 0 and the { id: 5 } object is level 4 - but
|
|
74
|
+
* a subarray or subobject inside { id: 5 } would render as the placeholder
|
|
75
|
+
*/
|
|
76
|
+
export let maxlevel = 5
|
|
77
|
+
/**
|
|
78
|
+
* take over the display of any leaf value; receives the default rendering
|
|
79
|
+
* (datetimes already rewritten as local ISO8601, everything else stringified)
|
|
80
|
+
* plus the value's path, so it can do something else with specific fields or
|
|
81
|
+
* types; return undefined to keep the default rendering
|
|
82
|
+
*/
|
|
83
|
+
export let format: ((value: string, path: (string | number)[]) => string | undefined) | undefined = undefined
|
|
84
|
+
/** the placeholders shown in place of data nested deeper than maxlevel, distinct so the reader knows what kind of data is hiding */
|
|
85
|
+
export let elidedObjectText = '{ ... }'
|
|
86
|
+
export let elidedArrayText = '[...]'
|
|
87
|
+
export let elidedTooltip = 'deeper data not shown'
|
|
88
|
+
export let className = ''
|
|
89
|
+
/**
|
|
90
|
+
* the containers above this one, used internally to detect cycles; a container
|
|
91
|
+
* that appears among its own ancestors renders as the placeholder immediately
|
|
92
|
+
* instead of repeating itself down to maxlevel
|
|
93
|
+
*/
|
|
94
|
+
export let ancestors: any[] = []
|
|
95
|
+
|
|
96
|
+
$: level = path.length
|
|
97
|
+
$: arrayData = arrayEntries(data)
|
|
98
|
+
$: objectData = arrayData == null ? objectEntries(data) : undefined
|
|
99
|
+
$: elided = level >= maxlevel || ancestors.includes(data)
|
|
100
|
+
$: childAncestors = [...ancestors, data]
|
|
101
|
+
|
|
102
|
+
function formatValue (value: any, atPath: (string | number)[]) {
|
|
103
|
+
const str = renderScalar(value)
|
|
104
|
+
return format?.(str, atPath) ?? str
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
{#if arrayData}
|
|
109
|
+
{#if elided}
|
|
110
|
+
<span class="nested-data-elided {className}" title={elidedTooltip}>{elidedArrayText}</span>
|
|
111
|
+
{:else if arrayData.length}
|
|
112
|
+
<ul class="nested-data {className}">
|
|
113
|
+
{#each arrayData as entry, i (i)}
|
|
114
|
+
<li><svelte:self data={entry} path={[...path, i]} ancestors={childAncestors} {maxlevel} {format} {elidedObjectText} {elidedArrayText} {elidedTooltip} /></li>
|
|
115
|
+
{/each}
|
|
116
|
+
</ul>
|
|
117
|
+
{/if}
|
|
118
|
+
{:else if objectData}
|
|
119
|
+
{#if elided}
|
|
120
|
+
<span class="nested-data-elided {className}" title={elidedTooltip}>{elidedObjectText}</span>
|
|
121
|
+
{:else}
|
|
122
|
+
<dl class="nested-data {className}">
|
|
123
|
+
{#each objectData as [key, value], i (i)}
|
|
124
|
+
<div>
|
|
125
|
+
<dt>{key}:</dt>
|
|
126
|
+
<dd><svelte:self data={value} path={[...path, key]} ancestors={childAncestors} {maxlevel} {format} {elidedObjectText} {elidedArrayText} {elidedTooltip} /></dd>
|
|
127
|
+
</div>
|
|
128
|
+
{/each}
|
|
129
|
+
</dl>
|
|
130
|
+
{/if}
|
|
131
|
+
{:else if data != null && (typeof data !== 'object' || data instanceof Date) && typeof data !== 'function'}
|
|
132
|
+
{formatValue(data, path)}
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
<style>
|
|
136
|
+
.nested-data {
|
|
137
|
+
margin: 0;
|
|
138
|
+
padding: 0;
|
|
139
|
+
line-height: 1.3;
|
|
140
|
+
}
|
|
141
|
+
ul.nested-data {
|
|
142
|
+
margin-left: 0.75em;
|
|
143
|
+
padding-left: 0.9em;
|
|
144
|
+
list-style: disc;
|
|
145
|
+
}
|
|
146
|
+
dl.nested-data dt {
|
|
147
|
+
display: inline;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
dl.nested-data dd {
|
|
151
|
+
display: inline;
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
/*
|
|
155
|
+
* an object directly beneath a key drops below it, indented a step; the child
|
|
156
|
+
* combinator keeps the indent from leaking into deeper levels, so an object
|
|
157
|
+
* inside an array entry hangs on the li's bullet with no extra margin
|
|
158
|
+
*/
|
|
159
|
+
dl.nested-data dd > :global(dl.nested-data) {
|
|
160
|
+
margin-left: 0.75em;
|
|
161
|
+
}
|
|
162
|
+
.nested-data-elided {
|
|
163
|
+
opacity: 0.65;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
data: any;
|
|
5
|
+
/** where this value sits in the root data, e.g. ['sections', 0, 'heading']; the root is [] */ path?: (string | number)[];
|
|
6
|
+
/**
|
|
7
|
+
* how many nested arrays/objects deep we render before eliding with a
|
|
8
|
+
* placeholder
|
|
9
|
+
*
|
|
10
|
+
* e.g. with the default of 5, { items: [{ moreitems: [{ id: 5 }] }] } displays
|
|
11
|
+
* fully - the root object is level 0 and the { id: 5 } object is level 4 - but
|
|
12
|
+
* a subarray or subobject inside { id: 5 } would render as the placeholder
|
|
13
|
+
*/ maxlevel?: number;
|
|
14
|
+
/**
|
|
15
|
+
* take over the display of any leaf value; receives the default rendering
|
|
16
|
+
* (datetimes already rewritten as local ISO8601, everything else stringified)
|
|
17
|
+
* plus the value's path, so it can do something else with specific fields or
|
|
18
|
+
* types; return undefined to keep the default rendering
|
|
19
|
+
*/ format?: ((value: string, path: (string | number)[]) => string | undefined) | undefined;
|
|
20
|
+
/** the placeholders shown in place of data nested deeper than maxlevel, distinct so the reader knows what kind of data is hiding */ elidedObjectText?: string;
|
|
21
|
+
elidedArrayText?: string;
|
|
22
|
+
elidedTooltip?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* the containers above this one, used internally to detect cycles; a container
|
|
26
|
+
* that appears among its own ancestors renders as the placeholder immediately
|
|
27
|
+
* instead of repeating itself down to maxlevel
|
|
28
|
+
*/ ancestors?: any[];
|
|
29
|
+
};
|
|
30
|
+
events: {
|
|
31
|
+
[evt: string]: CustomEvent<any>;
|
|
32
|
+
};
|
|
33
|
+
slots: {};
|
|
34
|
+
};
|
|
35
|
+
export type NestedDataProps = typeof __propDef.props;
|
|
36
|
+
export type NestedDataEvents = typeof __propDef.events;
|
|
37
|
+
export type NestedDataSlots = typeof __propDef.slots;
|
|
38
|
+
/**
|
|
39
|
+
* Displays arbitrary JSON-ish data as a compact nested structure: property lists
|
|
40
|
+
* for objects (and Maps), bulleted lists for arrays (and Sets), scalars as text.
|
|
41
|
+
* Intended for read-only summaries like preview panes, not for editing.
|
|
42
|
+
*
|
|
43
|
+
* Notes on odd input:
|
|
44
|
+
* - datetimes render as ISO8601 in the browser's timezone, whether they arrive as
|
|
45
|
+
* Date objects or ISO strings (e.g. UTC from a JSON API); use `format` to display
|
|
46
|
+
* them some other way
|
|
47
|
+
* - a Map renders like an object as long as every key is a string or has a custom
|
|
48
|
+
* toString; otherwise the Map is skipped entirely
|
|
49
|
+
* - cyclic structures are safe: a container that appears among its own ancestors
|
|
50
|
+
* renders the placeholder at the point of the cycle
|
|
51
|
+
* - empty arrays/objects, functions, and null/undefined render nothing
|
|
52
|
+
*/
|
|
53
|
+
export default class NestedData extends SvelteComponentTyped<NestedDataProps, NestedDataEvents, NestedDataSlots> {
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -8,5 +8,6 @@ export { default as Loading } from './Loading.svelte';
|
|
|
8
8
|
export { default as Lottie } from './Lottie.svelte';
|
|
9
9
|
export { default as Modal } from './Modal.svelte';
|
|
10
10
|
export { default as MultiSelect } from './MultiSelect.svelte';
|
|
11
|
+
export { default as NestedData } from './NestedData.svelte';
|
|
11
12
|
export { default as PopupMenu } from './PopupMenu.svelte';
|
|
12
13
|
export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -8,5 +8,6 @@ export { default as Loading } from './Loading.svelte';
|
|
|
8
8
|
export { default as Lottie } from './Lottie.svelte';
|
|
9
9
|
export { default as Modal } from './Modal.svelte';
|
|
10
10
|
export { default as MultiSelect } from './MultiSelect.svelte';
|
|
11
|
+
export { default as NestedData } from './NestedData.svelte';
|
|
11
12
|
export { default as PopupMenu } from './PopupMenu.svelte';
|
|
12
13
|
export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
|