@record-evolution/widget-form 1.0.32 → 1.0.34

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.
@@ -23,6 +23,84 @@ type Theme = {
23
23
  theme_name: string
24
24
  theme_object: any
25
25
  }
26
+
27
+ // The HTML `pattern` attribute is compiled with the RegExp `v` flag, which
28
+ // reserves these characters inside a character class: they have to be escaped
29
+ // even where the older `u` flag accepted them bare.
30
+ const V_RESERVED_IN_CLASS = new Set(['(', ')', '[', '{', '}', '/', '|'])
31
+
32
+ // Escape the characters a `v`-flag character class reserves but that were
33
+ // plainly meant literally, so a regex written against the older semantics —
34
+ // `^[a-zA-Z0-9 _-]+$` and friends — keeps working. A hyphen is left alone
35
+ // where it sits between two class atoms, because there it is a range operator.
36
+ const escapeForVFlag = (pattern: string): string => {
37
+ let out = ''
38
+ let inClass = false
39
+ // Index of the first atom of the current class, i.e. past `[` and any `^`.
40
+ let classContentStart = 0
41
+ for (let i = 0; i < pattern.length; i++) {
42
+ const char = pattern[i]
43
+ if (char === '\\') {
44
+ out += char + (pattern[i + 1] ?? '')
45
+ i++
46
+ } else if (!inClass) {
47
+ if (char === '[') {
48
+ inClass = true
49
+ classContentStart = pattern[i + 1] === '^' ? i + 2 : i + 1
50
+ }
51
+ out += char
52
+ } else if (char === ']') {
53
+ inClass = false
54
+ out += char
55
+ } else if (char === '-') {
56
+ const isRange = i > classContentStart && i < pattern.length - 1 && pattern[i + 1] !== ']'
57
+ out += isRange ? char : '\\-'
58
+ } else {
59
+ out += V_RESERVED_IN_CLASS.has(char) ? '\\' + char : char
60
+ }
61
+ }
62
+ return out
63
+ }
64
+
65
+ const compilesAsPattern = (pattern: string): boolean => {
66
+ try {
67
+ // Same shape the HTML spec compiles the `pattern` attribute with.
68
+ new RegExp('^(?:' + pattern + ')$', 'v')
69
+ return true
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ const patternCache = new Map<string, string>()
76
+
77
+ // A regex the browser cannot compile makes the text field throw a SyntaxError
78
+ // on every validity read, which kills validation reporting and form submission
79
+ // for the whole form. Repair what can be repaired, and drop the rest so a
80
+ // mis-escaped config costs one field's validation instead of the form.
81
+ const browserSafePattern = (raw?: string | null): string => {
82
+ const pattern = raw?.trim()
83
+ if (!pattern) return ''
84
+ const cached = patternCache.get(pattern)
85
+ if (cached !== undefined) return cached
86
+
87
+ let safe = ''
88
+ if (compilesAsPattern(pattern)) {
89
+ safe = pattern
90
+ } else {
91
+ const escaped = escapeForVFlag(pattern)
92
+ if (compilesAsPattern(escaped)) {
93
+ safe = escaped
94
+ } else {
95
+ console.warn(
96
+ `widget-form: ignoring validation regex "${pattern}" — it is not a valid regular expression under the "v" flag the HTML pattern attribute uses.`
97
+ )
98
+ }
99
+ }
100
+ patternCache.set(pattern, safe)
101
+ return safe
102
+ }
103
+
26
104
  @customElement('widget-form-versionplaceholder')
27
105
  export class WidgetForm extends LitElement {
28
106
  @property({ type: Object })
@@ -100,10 +178,8 @@ export class WidgetForm extends LitElement {
100
178
  }
101
179
 
102
180
  registerTheme(theme?: Theme) {
103
- const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim()
104
- const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim()
105
- this.themeBgColor = cssBgColor || this.theme?.theme_object?.backgroundColor
106
- this.themeTitleColor = cssTextColor || this.theme?.theme_object?.title?.textStyle?.color
181
+ this.themeBgColor = `var(--re-tile-background-color, ${this.theme?.theme_object?.backgroundColor || 'transparent'})`
182
+ this.themeTitleColor = `var(--re-text-color, ${this.theme?.theme_object?.title?.textStyle?.color || 'inherit'})`
107
183
  }
108
184
 
109
185
  openFormDialog() {
@@ -268,7 +344,7 @@ export class WidgetForm extends LitElement {
268
344
  .type="${field.type === 'numberfield' ? 'number' : 'text'}"
269
345
  .value="${field.preFilledValue ?? ''}"
270
346
  .placeholder="${field.defaultValue ?? ''}"
271
- .pattern="${field.validation ?? ''}"
347
+ .pattern="${browserSafePattern(field.validation)}"
272
348
  supporting-text=${field.description ?? ''}
273
349
  validation-message="${field.validationMessage ?? 'Invalid input'}"
274
350
  ?required=${field.required && !field.defaultValue && !field.preFilledValue}
@@ -578,16 +654,32 @@ export class WidgetForm extends LitElement {
578
654
  }
579
655
  `
580
656
 
657
+ /**
658
+ * Resolved background colour, for the alpha-stripping below.
659
+ *
660
+ * themeBgColor holds a var() chain so the tile tracks the host's
661
+ * --re-tile-background-color live, but a chain cannot be picked apart in
662
+ * JS. The opaque variant therefore resolves the colour here, at render
663
+ * time, instead of reading a value cached at the last theme change.
664
+ */
665
+ private resolvedBgColor(): string | undefined {
666
+ return (
667
+ getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim() ||
668
+ this.theme?.theme_object?.backgroundColor
669
+ )
670
+ }
671
+
581
672
  render() {
582
673
  const fontColor = this.themeTitleColor
583
674
  const bgColor = this.themeBgColor
584
- const bgColorOpaque = bgColor?.startsWith('rgba')
585
- ? bgColor.replace(/rgba\(([^)]+),\s*[\d.]+\)/, 'rgb($1)')
586
- : bgColor?.startsWith('#') && bgColor.length === 9
587
- ? bgColor.substring(0, 7) // #RRGGBBAA -> #RRGGBB
588
- : bgColor?.startsWith('#') && bgColor.length === 5
589
- ? bgColor.substring(0, 4) // #RGBA -> #RGB
590
- : bgColor
675
+ const opaqueSource = this.resolvedBgColor()
676
+ const bgColorOpaque = opaqueSource?.startsWith('rgba')
677
+ ? opaqueSource.replace(/rgba\(([^)]+),\s*[\d.]+\)/, 'rgb($1)')
678
+ : opaqueSource?.startsWith('#') && opaqueSource.length === 9
679
+ ? opaqueSource.substring(0, 7) // #RRGGBBAA -> #RRGGBB
680
+ : opaqueSource?.startsWith('#') && opaqueSource.length === 5
681
+ ? opaqueSource.substring(0, 4) // #RGBA -> #RGB
682
+ : opaqueSource
591
683
  return html`
592
684
  <style>
593
685
  :host {