@txstate-mws/svelte-components 1.7.1 → 1.7.3

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.
@@ -65,7 +65,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
65
65
  parent.style.minHeight = String(minHeight) + 'px';
66
66
  }
67
67
  }
68
- let lastrect;
68
+ let lastmeasured;
69
69
  // The containing block (nearest ancestor that establishes a containing block for
70
70
  // position: fixed) is determined by the ancestors' computed styles. Scrolling can
71
71
  // never change it, but it CAN change when an ancestor gains/loses a transform,
@@ -84,19 +84,33 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
84
84
  }
85
85
  let fixedRect = { left: 0, top: 0, right: 0, bottom: 0 };
86
86
  if (fixedParent) {
87
+ // fixed elements are positioned against the containing block's padding box, so
88
+ // measure from inside its borders
87
89
  const tmpRect = fixedParent.getBoundingClientRect();
88
- fixedRect = { left: tmpRect.left, right: document.documentElement.clientWidth - tmpRect.right, top: tmpRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom };
90
+ const left = tmpRect.left + fixedParent.clientLeft;
91
+ const top = tmpRect.top + fixedParent.clientTop;
92
+ fixedRect = { left, right: document.documentElement.clientWidth - left - fixedParent.clientWidth, top, bottom: document.documentElement.clientHeight - top - fixedParent.clientHeight };
89
93
  }
90
94
  if (!target)
91
95
  return undefined;
92
96
  const tmpRect = target.getBoundingClientRect();
93
- const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
94
- if (equal(rect, lastrect))
97
+ const vpWidth = document.documentElement.clientWidth;
98
+ const vpHeight = document.documentElement.clientHeight;
99
+ // The space available around the target in the viewport, used to make the auto
100
+ // alignment decision. This must not be relative to the fixed containing block: an
101
+ // ancestor with a transform, container-type, etc. changes what our CSS offsets are
102
+ // measured from, but it doesn't clip us, so the space that matters is the viewport.
103
+ const space = { left: tmpRect.left, right: vpWidth - tmpRect.right, top: tmpRect.top, bottom: vpHeight - tmpRect.bottom };
104
+ // The target's offsets from the edges of the fixed containing block, used to write
105
+ // the CSS position values.
106
+ const rect = { left: tmpRect.left - fixedRect.left, right: vpWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: vpHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
107
+ const measured = { rect, space };
108
+ if (equal(measured, lastmeasured))
95
109
  return undefined;
96
- lastrect = rect;
110
+ lastmeasured = measured;
97
111
  let autoalign = align;
98
- const leftright = rect.right > rect.left ? 'left' : 'right';
99
- const topbottom = rect.bottom > rect.top ? 'bottom' : 'top';
112
+ const leftright = space.right > space.left ? 'left' : 'right';
113
+ const topbottom = space.bottom > space.top ? 'bottom' : 'top';
100
114
  if (align === 'auto') {
101
115
  autoalign = topbottom + leftright;
102
116
  }
@@ -123,12 +137,12 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
123
137
  autoalign = 'middle';
124
138
  }
125
139
  else {
126
- const maxSpace = Math.max(rect.top, rect.bottom, rect.left, rect.right);
127
- if (maxSpace === rect.bottom)
140
+ const maxSpace = Math.max(space.top, space.bottom, space.left, space.right);
141
+ if (maxSpace === space.bottom)
128
142
  autoalign = 'bottom';
129
- else if (maxSpace === rect.top)
143
+ else if (maxSpace === space.top)
130
144
  autoalign = 'top';
131
- else if (maxSpace === rect.right)
145
+ else if (maxSpace === space.right)
132
146
  autoalign = 'right';
133
147
  else
134
148
  autoalign = 'left';
@@ -304,14 +318,12 @@ function isFixedContainer(el) {
304
318
  const css = getComputedStyle(el);
305
319
  if (css.getPropertyValue('transform') !== 'none' || css.getPropertyValue('perspective') !== 'none')
306
320
  return true; // A transform or perspective value other than none
307
- if (['transform', 'perspective', 'filter'].includes(css.getPropertyValue('will-change')))
308
- return true; // A will-change value of transform, filter, or perspective
321
+ if (css.getPropertyValue('will-change').split(/,\s*/).some(v => ['transform', 'perspective', 'filter'].includes(v)))
322
+ return true; // A will-change value including transform, filter, or perspective (e.g. will-change: opacity, transform;)
309
323
  if (css.getPropertyValue('filter') !== 'none')
310
324
  return true; // A filter value other than none
311
- if (['layout', 'paint', 'strict', 'content'].includes(css.getPropertyValue('contain')))
312
- return true; // A contain value of layout, paint, strict or content (e.g. contain: paint;)
313
- if ('container-type' in css && css.getPropertyValue('container-type') !== 'normal')
314
- return true; // A container-type value other than normal
325
+ if (css.getPropertyValue('contain').split(/\s+/).some(v => ['layout', 'paint', 'strict', 'content'].includes(v)))
326
+ return true; // A contain value including layout, paint, strict or content (e.g. contain: paint; or contain: size layout;)
315
327
  if ('backdrop-filter' in css && css.getPropertyValue('backdrop-filter') !== 'none')
316
328
  return true; // A backdrop-filter other than none (e.g. backdrop-filter: blur(10px);)
317
329
  return false;
@@ -88,11 +88,23 @@
88
88
  export let maxlevel = 5
89
89
  /**
90
90
  * take over the display of any leaf value; receives the default rendering
91
- * (datetimes already rewritten as local ISO8601, everything else stringified)
92
- * plus the value's path, so it can do something else with specific fields or
93
- * types; return undefined to keep the default rendering
91
+ * (datetimes already rewritten as local ISO8601, everything else
92
+ * stringified) plus the value's path, so it can do something else with
93
+ * specific fields or types - including recognizing marker strings a data
94
+ * source deliberately embedded, like '{"image": "https://..."}'
95
+ *
96
+ * return a string to display as text, return { html } to display markup the
97
+ * formatter built itself - it renders regardless of allowHtml, because the
98
+ * formatter is application code rather than data, though it is still
99
+ * scrubbed with stripUnsafeHtml since the urls and labels interpolated into
100
+ * it usually do come from the data - or return undefined to keep the
101
+ * default rendering
102
+ *
103
+ * { html } drops below its key as an indented block by default; pass
104
+ * inline: true for something compact like a link that should sit beside its
105
+ * key the way a short scalar does
94
106
  */
95
- export let format: ((value: string, path: (string | number)[]) => string | undefined) | undefined = undefined
107
+ export let format: ((value: string, path: (string | number)[]) => string | { html: string, inline?: boolean } | undefined) | undefined = undefined
96
108
  /**
97
109
  * leaf values longer than this many characters are cut off with an ellipsis,
98
110
  * after `format` has had its say; set Infinity if you really want it all
@@ -144,12 +156,21 @@
144
156
  * so it stays beside the key, and a scalar whose rendered text contains line
145
157
  * breaks (or is long enough that wrapping is inevitable) drops below the key,
146
158
  * so the breaks don't leave text hanging in the middle of the row
159
+ *
160
+ * formatted values follow the same rules: { html } is a block unless the
161
+ * formatter flagged it inline, text follows the scalar rules
147
162
  */
148
163
  function isBlockValue (value: any, key: string) {
164
+ const formatted = applyFormat(value, [...path, key])
165
+ if (formatted != null) {
166
+ if (typeof formatted === 'object') return !formatted.inline
167
+ const t = truncate(formatted)
168
+ return t.includes('\n') || t.length > BLOCKTEXT
169
+ }
149
170
  if (level + 1 >= maxlevel || childAncestors.includes(value)) return false
150
171
  if (arrayEntries(value) != null || objectEntries(value) != null) return true
151
172
  if (typeof value !== 'string') return false
152
- const rendered = formatValue(value, [...path, key])
173
+ const rendered = formatValue(value)
153
174
  return isHtml(rendered) || rendered.includes('\n') || rendered.length > BLOCKTEXT
154
175
  }
155
176
 
@@ -165,16 +186,41 @@
165
186
  let htmlContentHeight = 0
166
187
  $: htmlClipped = htmlContentHeight > htmlClipHeight + 1
167
188
 
168
- function formatValue (value: any, atPath: (string | number)[]) {
189
+ function truncate (str: string) {
190
+ return str.length > maxtext ? str.slice(0, maxtext).trimEnd() + '…' : str
191
+ }
192
+
193
+ /**
194
+ * run the format prop on a leaf value's default rendering; an { html }
195
+ * result needs DOMParser for scrubbing, so without one (i.e. during SSR) it
196
+ * is ignored and the value renders the default way until the browser
197
+ * hydrates
198
+ */
199
+ function applyFormat (value: any, atPath: (string | number)[]): string | { html: string, inline?: boolean } | undefined {
200
+ if (format == null) return undefined
201
+ if (value == null || (typeof value === 'object' && !(value instanceof Date)) || typeof value === 'function') return undefined
202
+ const result = format(renderScalar(value), atPath)
203
+ if (result != null && typeof result === 'object' && typeof DOMParser === 'undefined') return undefined
204
+ return result
205
+ }
206
+
207
+ $: formatted = applyFormat(data, path)
208
+ $: formattedHtml = formatted != null && typeof formatted === 'object' ? stripUnsafeHtml(formatted.html) : undefined
209
+ $: formattedText = typeof formatted === 'string' ? truncate(formatted) : undefined
210
+
211
+ function formatValue (value: any) {
169
212
  const str = renderScalar(value)
170
- const formatted = format?.(str, atPath) ?? str
171
213
  // never truncate html - slicing markup in half would break it
172
- if (isHtml(formatted)) return formatted
173
- return formatted.length > maxtext ? formatted.slice(0, maxtext).trimEnd() + '…' : formatted
214
+ if (isHtml(str)) return str
215
+ return truncate(str)
174
216
  }
175
217
  </script>
176
218
 
177
- {#if arrayData}
219
+ {#if formattedHtml != null}
220
+ <span class="nested-data-scalar nested-data-trusted {className}">{@html formattedHtml}</span>
221
+ {:else if formattedText != null}
222
+ <span class="nested-data-scalar {className}">{formattedText}</span>
223
+ {:else if arrayData}
178
224
  {#if elided}
179
225
  <span class="nested-data-elided {className}" title={elidedTooltip}>{elidedArrayText}</span>
180
226
  {:else if arrayData.length}
@@ -198,19 +244,19 @@
198
244
  </dl>
199
245
  {/if}
200
246
  {:else if data != null && (typeof data !== 'object' || data instanceof Date) && typeof data !== 'function'}
201
- {#if isHtml(formatValue(data, path))}
247
+ {#if isHtml(formatValue(data))}
202
248
  <!-- the frame and legend keep rendered markup (especially lists) from being mistaken for the data's own structure -->
203
249
  <fieldset class="nested-data-scalar nested-data-html {className}" class:nested-data-clipped={htmlClipped}>
204
250
  <legend>HTML</legend>
205
251
  <div class="nested-data-html-clip" bind:clientHeight={htmlClipHeight}>
206
- <div bind:offsetHeight={htmlContentHeight}>{@html stripUnsafeHtml(formatValue(data, path))}</div>
252
+ <div bind:offsetHeight={htmlContentHeight}>{@html stripUnsafeHtml(formatValue(data))}</div>
207
253
  </div>
208
254
  {#if htmlClipped}
209
255
  <div class="nested-data-html-more" title={clippedTooltip}>&hellip;</div>
210
256
  {/if}
211
257
  </fieldset>
212
258
  {:else}
213
- <span class="nested-data-scalar {className}">{formatValue(data, path)}</span>
259
+ <span class="nested-data-scalar {className}">{formatValue(data)}</span>
214
260
  {/if}
215
261
  {/if}
216
262
 
@@ -253,6 +299,14 @@
253
299
  white-space: pre-line;
254
300
  overflow-wrap: break-word;
255
301
  }
302
+ /* markup built by the format prop renders inline, unframed */
303
+ .nested-data-trusted {
304
+ white-space: normal;
305
+ }
306
+ .nested-data-trusted :global(img) {
307
+ max-width: 100%;
308
+ vertical-align: middle;
309
+ }
256
310
  /* html brings its own paragraphs and breaks; pre-line would double them up */
257
311
  .nested-data-html {
258
312
  white-space: normal;
@@ -296,8 +350,8 @@
296
350
  .nested-data-html :global(p:last-of-type) {
297
351
  margin-bottom: 0;
298
352
  }
299
- /* de-emphasize blocks of text that have dropped below their key */
300
- dl.nested-data dd.nested-data-block > :global(.nested-data-scalar) {
353
+ /* de-emphasize blocks of text that have dropped below their key; formatter-built markup keeps full strength */
354
+ dl.nested-data dd.nested-data-block > :global(.nested-data-scalar:not(.nested-data-trusted)) {
301
355
  opacity: var(--nested-data-block-opacity, 0.85);
302
356
  }
303
357
  .nested-data-elided {
@@ -13,10 +13,25 @@ declare const __propDef: {
13
13
  */ maxlevel?: number;
14
14
  /**
15
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;
16
+ * (datetimes already rewritten as local ISO8601, everything else
17
+ * stringified) plus the value's path, so it can do something else with
18
+ * specific fields or types - including recognizing marker strings a data
19
+ * source deliberately embedded, like '{"image": "https://..."}'
20
+ *
21
+ * return a string to display as text, return { html } to display markup the
22
+ * formatter built itself - it renders regardless of allowHtml, because the
23
+ * formatter is application code rather than data, though it is still
24
+ * scrubbed with stripUnsafeHtml since the urls and labels interpolated into
25
+ * it usually do come from the data - or return undefined to keep the
26
+ * default rendering
27
+ *
28
+ * { html } drops below its key as an indented block by default; pass
29
+ * inline: true for something compact like a link that should sit beside its
30
+ * key the way a short scalar does
31
+ */ format?: ((value: string, path: (string | number)[]) => string | {
32
+ html: string;
33
+ inline?: boolean;
34
+ } | undefined) | undefined;
20
35
  /**
21
36
  * leaf values longer than this many characters are cut off with an ellipsis,
22
37
  * after `format` has had its say; set Infinity if you really want it all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",