@bagelink/vue 1.15.417 → 1.15.425

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.
@@ -2,15 +2,29 @@ import { Attributes, BglFormSchemaT, Path } from '../types/BagelForm';
2
2
  export declare function debounce(fn: () => void, wait?: number): void;
3
3
  export declare function slugify(str: string): string;
4
4
  export declare function keyToLabel(key?: string): string | undefined;
5
+ /**
6
+ * Convert stored rich-text HTML to compact plain text for tables, labels, and
7
+ * other read-only summaries. Block elements are separated so paragraphs do not
8
+ * run together.
9
+ */
10
+ export declare function richTextToPlainText(value: string): string;
11
+ export declare function containsHtml(value: string): boolean;
5
12
  export declare function copyText(text: string, cb?: (msg: string) => void): Promise<void>;
6
13
  export declare function initials(...strArr: string[]): string;
14
+ export interface SanitizeHtmlOptions {
15
+ /**
16
+ * Remove inline presentation and layout attributes while preserving semantic
17
+ * markup such as paragraphs, lists, emphasis, and links.
18
+ */
19
+ resetFormatting?: boolean;
20
+ }
7
21
  /**
8
22
  * Strip script-capable content from an HTML string so it is safe for v-html.
9
23
  * Removes <script>/<style>/<iframe>/<object>/<embed> elements, on* event
10
24
  * handler attributes, and javascript: / data: URLs in href/src.
11
25
  * Allowlist-free by design — formatting tags (b, i, a, span…) pass through.
12
26
  */
13
- export declare function sanitizeHtml(html: string): string;
27
+ export declare function sanitizeHtml(html: string, options?: SanitizeHtmlOptions): string;
14
28
  export declare function useEscape(event: KeyboardEvent, closeModel: () => void): void;
15
29
  export declare function classify(fieldVal?: any, row?: any, ...classes: any[]): string;
16
30
  export declare function bindAttrs<T, P extends Path<T>>(attrs?: Attributes<T, P>, fieldVal?: any, row?: T): {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.417",
4
+ "version": "1.15.425",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -61,7 +61,7 @@ const themed = computed(() => props.color
61
61
  >
62
62
  <Image v-if="src" :src="src" :alt="alt || name" />
63
63
  <Icon v-else-if="icon" :icon="icon" :size="resolvedSize / 40" />
64
- <p v-else :style="{ 'line-height': `${resolvedSize}px`, 'font-size': `calc(1.5rem * ${resolvedSize} / 50)` }">
64
+ <p v-else :style="{ 'line-height': '1', 'font-size': `calc(1.5rem * ${resolvedSize} / 50)` }" class="">
65
65
  {{ (fallback || initials(name || '')).toUpperCase() }}
66
66
  </p>
67
67
  </div>
@@ -3,7 +3,7 @@ import type { Ref, WritableComputedRef } from 'vue'
3
3
 
4
4
  import type { DraggableEvent } from '../../draggable/useDraggable'
5
5
 
6
- import { Btn, resolveI18n, useI18n, pathKeyToURL } from '@bagelink/vue'
6
+ import { Btn, resolveI18n, useI18n, pathKeyToURL, richTextToPlainText } from '@bagelink/vue'
7
7
  import { computed, ref, watch, useSlots, type VNode } from 'vue'
8
8
  import Draggable from '../../draggable/Draggable.vue'
9
9
  import type { BagelInputShellProps } from './bagelInputShell'
@@ -48,26 +48,19 @@ const itemLabelPrefix = computed(() => {
48
48
  return $t('arrayInput.item')
49
49
  })
50
50
 
51
- const IDENTIFIER_FIELDS = ['title', 'name', 'label', 'displayName', 'display_name', 'heading', 'caption', 'text']
51
+ const IDENTIFIER_FIELDS = ['title', 'name', 'key', 'label', 'displayName', 'display_name', 'heading', 'caption', 'text']
52
52
  const IMAGE_FIELDS = ['image', 'thumbnail', 'avatar', 'photo', 'picture', 'profilePic', 'profile_pic', 'icon', 'logo']
53
53
 
54
- /** Rich-text fields often store HTML; collapse to plain text for the collapsed row label. */
55
- function toPlainText(value: string): string {
56
- if (!value.includes('<') && !value.includes('&')) return value.trim()
57
- const doc = new DOMParser().parseFromString(value, 'text/html')
58
- return (doc.body.textContent || '').replace(/\s+/g, ' ').trim()
59
- }
60
-
61
54
  function getItemIdentifier(item: T): string | null {
62
55
  if (typeof item === 'string' && item.trim().length > 0) {
63
- const plain = toPlainText(item)
56
+ const plain = richTextToPlainText(item)
64
57
  return plain.length > 0 ? plain : null
65
58
  }
66
59
  if (item !== null && item !== undefined && typeof item === 'object') {
67
60
  for (const field of IDENTIFIER_FIELDS) {
68
61
  const val = (item as Record<string, unknown>)[field]
69
62
  if (typeof val === 'string' && val.trim().length > 0) {
70
- const plain = toPlainText(val)
63
+ const plain = richTextToPlainText(val)
71
64
  if (plain.length > 0) return plain
72
65
  }
73
66
  }
@@ -38,6 +38,7 @@ const props = withDefaults(
38
38
  invertedActive?: boolean
39
39
  align?: 'start' | 'center' | 'end' | 'top' | 'bottom'
40
40
  hideRadio?: boolean
41
+ hideLabelWhenImage?: boolean
41
42
  bgColor?: string
42
43
  activeBgColor?: string
43
44
  activeTextColor?: string
@@ -189,10 +190,10 @@ function handleChange() {
189
190
  <div class="flex w-100 gap-1 flex-wrap m_gap-05 m_gap-row-025"
190
191
  :class="{ 'txt-center justify-content-center mx-auto': textAlign === 'center', 'radio-option-content--centered-image': textAlign === 'center' && opt.imgSrc, 'txt-end justify-content-end': textAlign === 'right', 'txt-start justify-content-start': textAlign === 'left' }"
191
192
  :style="{ color: textColor }">
192
- <img v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40" :src="opt.imgSrc" :alt="opt.imgAlt || opt.label || ''">
193
+ <img v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40" :src="opt.imgSrc" :alt="(hideLabelWhenImage ? opt.label || opt.imgAlt : opt.imgAlt || opt.label) || ''">
193
194
  <Icon v-else-if="opt.icon" :icon="opt.icon" :size="1" />
194
- <div class="">
195
- <div v-if="opt.label" class="m-0 m_txt-14 line-height-14" v-html="sanitizeHtml(opt.label)" />
195
+ <div v-if="(opt.label && !(hideLabelWhenImage && opt.imgSrc)) || opt.subLabel">
196
+ <div v-if="opt.label && !(hideLabelWhenImage && opt.imgSrc)" class="m-0 m_txt-14 line-height-14" v-html="sanitizeHtml(opt.label)" />
196
197
  <p v-if="opt.subLabel" class="txt-gray txt-12 m-0 pt-025">{{ opt.subLabel }}</p>
197
198
  </div>
198
199
  <slot name="radioItem" v-bind="opt" />
@@ -18,7 +18,9 @@ import {
18
18
  ColorInput,
19
19
  RangeInput,
20
20
  EmailInput,
21
- getNestedValue
21
+ containsHtml,
22
+ getNestedValue,
23
+ richTextToPlainText
22
24
  } from '@bagelink/vue'
23
25
  import { h, isVNode } from 'vue'
24
26
 
@@ -388,6 +390,22 @@ export function useSchemaField<T extends { [key: string]: any }>(optns: UseSchem
388
390
  ])
389
391
  }
390
392
 
393
+ if (!slotContent && mode === 'table' && typeof transformedValue === 'string') {
394
+ const plainText = richTextToPlainText(transformedValue)
395
+ if (containsHtml(transformedValue)) {
396
+ return h('div', { class: 'ellipsis-1', title: plainText }, plainText)
397
+ }
398
+ }
399
+
400
+ if (!slotContent && mode === 'table' && Component === RichText) {
401
+ const plainText = richTextToPlainText(String(transformedValue ?? ''))
402
+ return h(
403
+ 'div',
404
+ { class: 'ellipsis-1', title: plainText },
405
+ plainText
406
+ )
407
+ }
408
+
391
409
  return slotContent || h(Component as any, props, componentSlots)
392
410
  }
393
411
 
@@ -15,7 +15,7 @@
15
15
  * button is always "Submit").
16
16
  */
17
17
 
18
- import type { FormStructure } from './structure'
18
+ import type { FormStep, FormStructure } from './structure'
19
19
  import type { Slots } from 'vue'
20
20
  import { computed, ref, useSlots, watch } from 'vue'
21
21
  import { useI18n } from '../i18n'
@@ -53,6 +53,15 @@ export interface Props {
53
53
  hideCounter?: boolean
54
54
  /** Optional async submit handler; awaited before `complete` is emitted. */
55
55
  onComplete?: (data: Record<string, any>) => Promise<void> | void
56
+ /**
57
+ * Runs after the current step validates but before advancing or submitting.
58
+ * Return false when the host handled navigation and the player should stop.
59
+ */
60
+ beforePrimary?: (context: {
61
+ data: Record<string, any>
62
+ currentStep: FormStep
63
+ isTerminal: boolean
64
+ }) => Promise<boolean | void> | boolean | void
56
65
  /** Resolve declarative x-options-source fields in stored JSON Schemas. */
57
66
  loadOptionsSource?: JSONSchemaOptionsSourceLoader
58
67
  }
@@ -182,9 +191,16 @@ function validateStep(): boolean {
182
191
  async function onPrimary() {
183
192
  if (!validateStep()) return
184
193
 
194
+ const step = currentStep.value
195
+ const proceed = await props.beforePrimary?.({
196
+ data: { ...formData.value },
197
+ currentStep: step!,
198
+ isTerminal: isTerminal.value,
199
+ })
200
+ if (proceed === false) return
201
+
185
202
  if (isTerminal.value) {
186
203
  submitting.value = true
187
- const step = currentStep.value
188
204
  try {
189
205
  // `skipSubmit` terminals (e.g. disqualification leaves) end the flow
190
206
  // without creating a submission — the host's onComplete is bypassed,
@@ -331,6 +347,7 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
331
347
  :step-position="stepPosition"
332
348
  :progress="progress"
333
349
  :current-key="currentKey"
350
+ :primary-label="currentStep?.primaryLabel"
334
351
  :auto-advances="autoAdvances"
335
352
  >
336
353
  <div class="ffp-nav flex flex-center gap-1">
@@ -353,7 +370,7 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
353
370
  <Btn
354
371
  v-if="isTerminal"
355
372
  class="ffp-nav-btn"
356
- :value="$t?.('multiStep.complete') ?? 'Submit'"
373
+ :value="currentStep?.primaryLabel || $t?.('multiStep.complete') || 'Submit'"
357
374
  :loading="submitting"
358
375
  @click="onPrimary"
359
376
  >
@@ -1259,6 +1259,7 @@ function applyDesignerConfig(config: Record<string, any>, prop: JSONSchemaProper
1259
1259
  if (p['x-radio-inline']) config.inline = true
1260
1260
  if (p['x-radio-thin']) config.thin = true
1261
1261
  if (p['x-radio-hide-radio']) config.hideRadio = true
1262
+ if (p['x-radio-hide-label-when-image']) config.hideLabelWhenImage = true
1262
1263
  set('align', 'x-radio-align')
1263
1264
  set('textAlign', 'x-radio-text-align')
1264
1265
  // Select
@@ -99,6 +99,7 @@ export interface FormField {
99
99
  rangeMulti?: boolean
100
100
  radioThin?: boolean
101
101
  radioHideRadio?: boolean
102
+ radioHideLabelWhenImage?: boolean
102
103
  radioAlign?: 'start' | 'center' | 'end'
103
104
  /** Lay radio options out horizontally (inline) instead of stacked. */
104
105
  radioInline?: boolean
@@ -165,6 +166,7 @@ export interface JSONSchemaProperty {
165
166
  'x-range-multi'?: boolean
166
167
  'x-radio-thin'?: boolean
167
168
  'x-radio-hide-radio'?: boolean
169
+ 'x-radio-hide-label-when-image'?: boolean
168
170
  'x-radio-align'?: 'start' | 'center' | 'end'
169
171
  'x-radio-inline'?: boolean
170
172
  'x-radio-size'?: 's' | 'm' | 'l'
@@ -293,6 +295,7 @@ export function schemaToFields(schema: JSONSchemaObject | undefined): FormField[
293
295
  rangeMulti: prop['x-range-multi'],
294
296
  radioThin: prop['x-radio-thin'],
295
297
  radioHideRadio: prop['x-radio-hide-radio'],
298
+ radioHideLabelWhenImage: prop['x-radio-hide-label-when-image'],
296
299
  radioAlign: prop['x-ui']?.radioAlign ?? prop['x-radio-align'],
297
300
  radioInline: prop['x-radio-inline'],
298
301
  radioSize: prop['x-radio-size'],
@@ -457,6 +460,7 @@ export function fieldsToSchema(
457
460
  if (field.rangeMulti === true) prop['x-range-multi'] = true
458
461
  if (field.radioThin === true) prop['x-radio-thin'] = true
459
462
  if (field.radioHideRadio === true) prop['x-radio-hide-radio'] = true
463
+ if (field.radioHideLabelWhenImage === true) prop['x-radio-hide-label-when-image'] = true
460
464
  if (field.radioAlign) prop['x-ui'] = { ...prop['x-ui'], radioAlign: field.radioAlign }
461
465
  if (field.radioInline === true) prop['x-radio-inline'] = true
462
466
  if (field.radioSize) prop['x-radio-size'] = field.radioSize
@@ -31,6 +31,8 @@ export interface FormStep {
31
31
  * (final step or an early-termination leaf). Empty => no redirect.
32
32
  */
33
33
  redirectUrl?: string | null
34
+ /** Optional primary-button label for this step (for example, "Next"). */
35
+ primaryLabel?: string | null
34
36
  /**
35
37
  * Terminal step that ends the flow *without* creating a submission — e.g. a
36
38
  * disqualification / "not a fit" leaf. The step's own content (title + fields)
@@ -4,6 +4,10 @@
4
4
  font-weight: 200;
5
5
  font-stretch: 100%;
6
6
  font-display: swap;
7
+ size-adjust: 110%;
8
+ ascent-override: 68%;
9
+ descent-override: 32%;
10
+ line-gap-override: 0%;
7
11
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultralight-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultralight-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultralight-aaa.eot) format('eot')
8
12
  }
9
13
 
@@ -13,6 +17,10 @@
13
17
  font-weight: 300;
14
18
  font-stretch: 100%;
15
19
  font-display: swap;
20
+ size-adjust: 110%;
21
+ ascent-override: 68%;
22
+ descent-override: 32%;
23
+ line-gap-override: 0%;
16
24
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-light-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-light-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-light-aaa.eot) format('eot')
17
25
  }
18
26
 
@@ -22,6 +30,10 @@
22
30
  font-weight: 400;
23
31
  font-stretch: 100%;
24
32
  font-display: swap;
33
+ size-adjust: 110%;
34
+ ascent-override: 68%;
35
+ descent-override: 32%;
36
+ line-gap-override: 0%;
25
37
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-regular-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-regular-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-regular-aaa.eot) format('eot')
26
38
  }
27
39
 
@@ -31,6 +43,10 @@
31
43
  font-weight: 500;
32
44
  font-stretch: 100%;
33
45
  font-display: swap;
46
+ size-adjust: 110%;
47
+ ascent-override: 68%;
48
+ descent-override: 32%;
49
+ line-gap-override: 0%;
34
50
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-medium-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-medium-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-medium-aaa.eot) format('eot')
35
51
  }
36
52
 
@@ -40,6 +56,10 @@
40
56
  font-weight: 600;
41
57
  font-stretch: 100%;
42
58
  font-display: swap;
59
+ size-adjust: 110%;
60
+ ascent-override: 68%;
61
+ descent-override: 32%;
62
+ line-gap-override: 0%;
43
63
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-demibold-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-demibold-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-demibold-aaa.eot) format('eot')
44
64
  }
45
65
 
@@ -49,6 +69,10 @@
49
69
  font-weight: 700;
50
70
  font-stretch: 100%;
51
71
  font-display: swap;
72
+ size-adjust: 110%;
73
+ ascent-override: 68%;
74
+ descent-override: 32%;
75
+ line-gap-override: 0%;
52
76
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-bold-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-bold-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-bold-aaa.eot) format('eot')
53
77
  }
54
78
 
@@ -58,6 +82,10 @@
58
82
  font-weight: 800;
59
83
  font-stretch: 100%;
60
84
  font-display: swap;
85
+ size-adjust: 110%;
86
+ ascent-override: 68%;
87
+ descent-override: 32%;
88
+ line-gap-override: 0%;
61
89
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultrabold-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultrabold-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-ultrabold-aaa.eot) format('eot')
62
90
  }
63
91
 
@@ -67,5 +95,9 @@
67
95
  font-weight: 900;
68
96
  font-stretch: 100%;
69
97
  font-display: swap;
98
+ size-adjust: 110%;
99
+ ascent-override: 68%;
100
+ descent-override: 32%;
101
+ line-gap-override: 0%;
70
102
  src: url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-black-aaa.woff2) format('woff2'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-black-aaa.woff) format('woff'), url(https://bagel.sfo2.cdn.digitaloceanspaces.com/Fonts/ploni/ploni-black-aaa.eot) format('eot')
71
103
  }
@@ -30,6 +30,40 @@ export function keyToLabel(key?: string): string | undefined {
30
30
  )
31
31
  }
32
32
 
33
+ const plainTextCache = new Map<string, string>()
34
+
35
+ /**
36
+ * Convert stored rich-text HTML to compact plain text for tables, labels, and
37
+ * other read-only summaries. Block elements are separated so paragraphs do not
38
+ * run together.
39
+ */
40
+ export function richTextToPlainText(value: string): string {
41
+ if (!value.includes('<') && !value.includes('&')) return value.trim()
42
+
43
+ const cached = plainTextCache.get(value)
44
+ if (cached !== undefined) return cached
45
+
46
+ if (typeof DOMParser === 'undefined') {
47
+ return value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
48
+ }
49
+
50
+ const doc = new DOMParser().parseFromString(value, 'text/html')
51
+ doc.body
52
+ .querySelectorAll('p, div, li, br, h1, h2, h3, h4, h5, h6')
53
+ .forEach(element => element.after(' '))
54
+ const result = (doc.body.textContent || '')
55
+ .replace(/\u00a0/g, ' ')
56
+ .replace(/\s+/g, ' ')
57
+ .trim()
58
+
59
+ plainTextCache.set(value, result)
60
+ return result
61
+ }
62
+
63
+ export function containsHtml(value: string): boolean {
64
+ return /<\/?[a-z][\s\S]*>/i.test(value)
65
+ }
66
+
33
67
  export async function copyText(text: string, cb?: (msg: string) => void) {
34
68
  await navigator.clipboard.writeText(text)
35
69
  if (cb) { cb('Copied to clipboard') }
@@ -40,13 +74,21 @@ export function initials(...strArr: string[]) {
40
74
  return strArr.map(str => str.charAt(0)).join('')
41
75
  }
42
76
 
77
+ export interface SanitizeHtmlOptions {
78
+ /**
79
+ * Remove inline presentation and layout attributes while preserving semantic
80
+ * markup such as paragraphs, lists, emphasis, and links.
81
+ */
82
+ resetFormatting?: boolean
83
+ }
84
+
43
85
  /**
44
86
  * Strip script-capable content from an HTML string so it is safe for v-html.
45
87
  * Removes <script>/<style>/<iframe>/<object>/<embed> elements, on* event
46
88
  * handler attributes, and javascript: / data: URLs in href/src.
47
89
  * Allowlist-free by design — formatting tags (b, i, a, span…) pass through.
48
90
  */
49
- export function sanitizeHtml(html: string): string {
91
+ export function sanitizeHtml(html: string, options: SanitizeHtmlOptions = {}): string {
50
92
  if (!html || typeof document === 'undefined') { return html }
51
93
  const template = document.createElement('template')
52
94
  template.innerHTML = html
@@ -64,6 +106,11 @@ export function sanitizeHtml(html: string): string {
64
106
  || ((name === 'href' || name === 'src' || name === 'xlink:href')
65
107
  && (value.startsWith('javascript:') || value.startsWith('data:')))) {
66
108
  child.removeAttribute(attr.name)
109
+ } else if (
110
+ options.resetFormatting
111
+ && ['style', 'class', 'width', 'height', 'align', 'bgcolor', 'color', 'face', 'size'].includes(name)
112
+ ) {
113
+ child.removeAttribute(attr.name)
67
114
  }
68
115
  }
69
116
  walk(child)