playbook_ui 16.4.0.pre.alpha.play2838formcustomvalidationsconsistency15347 → 16.4.0.pre.alpha.play287215277
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +46 -325
- data/app/pb_kits/playbook/pb_icon/_icon.tsx +4 -0
- data/app/pb_kits/playbook/pb_icon/icon.html.erb +1 -0
- data/app/pb_kits/playbook/pb_icon/icon.rb +23 -0
- data/app/pb_kits/playbook/utilities/iconFallbackWarning.ts +56 -0
- data/dist/chunks/_typeahead-azNI64Xs.js +1 -0
- data/dist/chunks/vendor.js +1 -1
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/lib/playbook/pb_forms_helper.rb +2 -24
- data/lib/playbook/version.rb +1 -1
- metadata +4 -3
- data/dist/chunks/_typeahead-BNp_YiTh.js +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f192477f8ce86f7e7d34c1fcd3e04ce3ec19bce384af58ee7b670ad2020bb7da
|
|
4
|
+
data.tar.gz: cdf3515b0efd18f633a9f0b9c971fba9f5e1c86ba26e1072be960e42d0dce68d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ea0752cc8fa230d382be06fa02a93b19830976599733888686a05cc85feccef9024d16debed509130b7902c7251c62b6e1f2604796b4e0b1ca8259f020738d0
|
|
7
|
+
data.tar.gz: 92b037ce1dded39bc8a28f90c79fdfbbcbff46056af9b9c111d9a51e43a1e2b60e441f9ac77c25ab022fd5a6a6c238d1800f92416935fb634550f877f527819e
|
|
@@ -9,8 +9,6 @@ const ERROR_MESSAGE_SELECTOR = '.pb_body_kit_negative'
|
|
|
9
9
|
const FORM_SELECTOR = 'form[data-pb-form-validation="true"]'
|
|
10
10
|
const REQUIRED_FIELDS_SELECTOR = 'input[required],textarea[required],select[required]'
|
|
11
11
|
const PHONE_NUMBER_VALIDATION_ERROR_SELECTOR = '[data-pb-phone-validation-error="true"]'
|
|
12
|
-
const FORM_VALIDATION_MESSAGE_ATTR = 'data-pb-form-validation-message'
|
|
13
|
-
const FORM_VALIDATION_MESSAGE_REUSED_ATTR = 'data-pb-form-validation-message-reused'
|
|
14
12
|
|
|
15
13
|
const FIELD_EVENTS = [
|
|
16
14
|
'change',
|
|
@@ -22,58 +20,26 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
22
20
|
return FORM_SELECTOR
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
static start() {
|
|
26
|
-
if (this.__pbStarted) return
|
|
27
|
-
this.__pbStarted = true
|
|
28
|
-
super.start()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
connect() {
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.bindValidationListeners()
|
|
37
|
-
this._setupMutationObserver()
|
|
38
|
-
this._setupSubmitHandler()
|
|
39
|
-
}
|
|
24
|
+
this.formValidationFields.forEach((field) => {
|
|
25
|
+
// Skip phone number inputs - they handle their own validation
|
|
26
|
+
const isPhoneNumberInput = field.closest('.pb_phone_number_input')
|
|
27
|
+
if (isPhoneNumberInput) return
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!this._pendingFieldsToBind?.size) return
|
|
45
|
-
this._pendingFieldsToBind.forEach((f) => this.bindFieldValidationListeners(f))
|
|
46
|
-
this._pendingFieldsToBind.clear()
|
|
47
|
-
}, 50)
|
|
29
|
+
// Skip TimePicker inputs - they handle their own validation
|
|
30
|
+
const isTimePickerInput = field.closest('.pb_time_picker')
|
|
31
|
+
if (isTimePickerInput) return
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!(node instanceof Element)) return
|
|
54
|
-
if (node.matches(REQUIRED_FIELDS_SELECTOR)) {
|
|
55
|
-
this._pendingFieldsToBind.add(node)
|
|
56
|
-
sawRequired = true
|
|
57
|
-
}
|
|
58
|
-
node.querySelectorAll?.(REQUIRED_FIELDS_SELECTOR)?.forEach?.((el) => {
|
|
59
|
-
this._pendingFieldsToBind.add(el)
|
|
60
|
-
sawRequired = true
|
|
61
|
-
})
|
|
62
|
-
})
|
|
33
|
+
FIELD_EVENTS.forEach((e) => {
|
|
34
|
+
field.addEventListener(e, debounce((event) => {
|
|
35
|
+
this.validateFormField(event)
|
|
36
|
+
}, 250), false)
|
|
63
37
|
})
|
|
64
|
-
if (sawRequired) this._flushPendingFieldBinds()
|
|
65
38
|
})
|
|
66
39
|
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
_setupSubmitHandler() {
|
|
40
|
+
// Add event listener to check for phone number validation errors
|
|
71
41
|
this.element.addEventListener('submit', (event) => {
|
|
72
|
-
|
|
73
|
-
event.preventDefault()
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
42
|
+
// Use setTimeout to ensure React state updates have completed
|
|
77
43
|
setTimeout(() => {
|
|
78
44
|
if (this.hasPhoneNumberValidationErrors()) {
|
|
79
45
|
event.preventDefault()
|
|
@@ -83,293 +49,61 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
83
49
|
})
|
|
84
50
|
}
|
|
85
51
|
|
|
86
|
-
validateOnSubmit() {
|
|
87
|
-
let foundInvalid = false
|
|
88
|
-
|
|
89
|
-
this.formValidationFields.forEach((field) => {
|
|
90
|
-
if (this.isTypeaheadField(field)) return
|
|
91
|
-
|
|
92
|
-
const isPhoneNumberInput = field.closest('.pb_phone_number_input')
|
|
93
|
-
if (isPhoneNumberInput) return
|
|
94
|
-
|
|
95
|
-
const isTimePickerInput = field.closest('.pb_time_picker')
|
|
96
|
-
if (isTimePickerInput) return
|
|
97
|
-
|
|
98
|
-
field.setCustomValidity('')
|
|
99
|
-
const ctx = this.getValidationContext(field)
|
|
100
|
-
|
|
101
|
-
if (field.validity.valid) {
|
|
102
|
-
this.clearError(field, ctx)
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const message = ctx.validationMessage
|
|
107
|
-
if (message) field.setCustomValidity(message)
|
|
108
|
-
|
|
109
|
-
foundInvalid = true
|
|
110
|
-
this.showValidationMessage(field, ctx)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
return foundInvalid
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
disconnect() {
|
|
117
|
-
this.mutationObserver?.disconnect()
|
|
118
|
-
|
|
119
|
-
this._pendingFieldsToBind?.clear()
|
|
120
|
-
this._pendingFieldsToBind = null
|
|
121
|
-
this._flushPendingFieldBinds = null
|
|
122
|
-
this._typeaheadFieldHandlers = null
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
bindValidationListeners() {
|
|
126
|
-
this.formValidationFields.forEach((field) => this.bindFieldValidationListeners(field))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
bindFieldValidationListeners(field) {
|
|
130
|
-
if (!field || !(field instanceof Element)) return
|
|
131
|
-
if (this.boundFields?.has(field)) return
|
|
132
|
-
if (this.isTypeaheadField(field)) {
|
|
133
|
-
this.bindTypeaheadValidationListeners(field)
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const isPhoneNumberInput = field.closest('.pb_phone_number_input')
|
|
138
|
-
if (isPhoneNumberInput) return
|
|
139
|
-
|
|
140
|
-
const isTimePickerInput = field.closest('.pb_time_picker')
|
|
141
|
-
if (isTimePickerInput) return
|
|
142
|
-
|
|
143
|
-
let handlers = this._fieldHandlers?.get(field)
|
|
144
|
-
if (!handlers) {
|
|
145
|
-
const debouncedHandler = debounce((event) => {
|
|
146
|
-
this.validateFormField(event)
|
|
147
|
-
}, 250)
|
|
148
|
-
|
|
149
|
-
const immediateHandler = (event) => {
|
|
150
|
-
this.validateFormField(event)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
handlers = { debouncedHandler, immediateHandler }
|
|
154
|
-
this._fieldHandlers?.set(field, handlers)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
FIELD_EVENTS.forEach((eventName) => {
|
|
158
|
-
if (eventName === 'invalid') {
|
|
159
|
-
field.addEventListener(eventName, handlers.immediateHandler, true)
|
|
160
|
-
} else if (eventName === 'change' && field.tagName === 'SELECT') {
|
|
161
|
-
field.addEventListener(eventName, handlers.immediateHandler, false)
|
|
162
|
-
} else {
|
|
163
|
-
field.addEventListener(eventName, handlers.debouncedHandler, false)
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
this.boundFields?.add(field)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
bindTypeaheadValidationListeners(field) {
|
|
171
|
-
if (!field || !(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement)) return
|
|
172
|
-
if (this.boundFields?.has(field)) return
|
|
173
|
-
|
|
174
|
-
let handlers = this._typeaheadFieldHandlers?.get(field)
|
|
175
|
-
if (!handlers) {
|
|
176
|
-
const applyCustomMessage = () => {
|
|
177
|
-
const kitElement = this.getKitElement(field)
|
|
178
|
-
const ctx = this.getValidationContext(field)
|
|
179
|
-
const message = this.getValidationMessage(field, kitElement)
|
|
180
|
-
|
|
181
|
-
field.setCustomValidity('')
|
|
182
|
-
if (!message) {
|
|
183
|
-
if (!this.isReactTypeaheadField(field)) this.showValidationMessage(field, ctx)
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (field.validity.valueMissing || !field.validity.valid) {
|
|
188
|
-
field.setCustomValidity(message)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (!this.isReactTypeaheadField(field)) {
|
|
192
|
-
this.showValidationMessage(field, ctx)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const clearCustomMessage = () => {
|
|
197
|
-
field.setCustomValidity('')
|
|
198
|
-
if (!this.isReactTypeaheadField(field)) {
|
|
199
|
-
this.clearError(field)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
handlers = { applyCustomMessage, clearCustomMessage }
|
|
204
|
-
this._typeaheadFieldHandlers?.set(field, handlers)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
field.addEventListener('invalid', handlers.applyCustomMessage, true)
|
|
208
|
-
field.addEventListener('input', handlers.clearCustomMessage, false)
|
|
209
|
-
field.addEventListener('change', handlers.clearCustomMessage, false)
|
|
210
|
-
|
|
211
|
-
this.boundFields?.add(field)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
52
|
validateFormField(event) {
|
|
215
53
|
event.preventDefault()
|
|
216
54
|
const { target } = event
|
|
217
|
-
if (this.isTypeaheadField(target)) return
|
|
218
|
-
|
|
219
|
-
// Suppress the native browser tooltip; PB shows its own inline error instead.
|
|
220
|
-
|
|
221
|
-
const ctx = this.getValidationContext(target)
|
|
222
|
-
|
|
223
55
|
target.setCustomValidity('')
|
|
224
|
-
|
|
225
|
-
const isValid = target.validity.valid
|
|
56
|
+
const isValid = event.target.validity.valid
|
|
226
57
|
|
|
227
58
|
if (isValid) {
|
|
228
|
-
this.clearError(target
|
|
59
|
+
this.clearError(target)
|
|
229
60
|
} else {
|
|
230
|
-
|
|
231
|
-
if (message) target.setCustomValidity(message)
|
|
232
|
-
this.showValidationMessage(target, ctx)
|
|
61
|
+
this.showValidationMessage(target)
|
|
233
62
|
}
|
|
234
63
|
}
|
|
235
64
|
|
|
236
|
-
|
|
237
|
-
const parentElement = target
|
|
238
|
-
const kitElement =
|
|
239
|
-
const controlWrapper = this.getControlWrapper(target, kitElement)
|
|
240
|
-
const errorParent = this.getErrorParent(target, kitElement, parentElement)
|
|
241
|
-
const validationMessage = this.getValidationMessage(target, kitElement)
|
|
65
|
+
showValidationMessage(target) {
|
|
66
|
+
const { parentElement } = target
|
|
67
|
+
const kitElement = parentElement.closest(KIT_SELECTOR)
|
|
242
68
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
kitElement,
|
|
246
|
-
controlWrapper,
|
|
247
|
-
errorParent,
|
|
248
|
-
validationMessage,
|
|
249
|
-
isPhoneNumberInput: !!kitElement?.classList?.contains('pb_phone_number_input'),
|
|
250
|
-
isTimePickerInput: !!kitElement?.classList?.contains('pb_time_picker'),
|
|
251
|
-
}
|
|
252
|
-
}
|
|
69
|
+
// FIX: Add null check for kitElement
|
|
70
|
+
if (!kitElement) return
|
|
253
71
|
|
|
254
|
-
|
|
255
|
-
const
|
|
72
|
+
// Check if this is a phone number input
|
|
73
|
+
const isPhoneNumberInput = kitElement.classList.contains('pb_phone_number_input')
|
|
74
|
+
|
|
75
|
+
// Check if this is a TimePicker input
|
|
76
|
+
const isTimePickerInput = kitElement.classList.contains('pb_time_picker')
|
|
256
77
|
|
|
257
78
|
// ensure clean error message state
|
|
258
|
-
this.clearError(target
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (controlWrapper) controlWrapper.classList.add('error')
|
|
79
|
+
this.clearError(target)
|
|
80
|
+
kitElement.classList.add('error')
|
|
262
81
|
|
|
82
|
+
// Only add error message if it's NOT a phone number input or TimePicker input
|
|
263
83
|
if (!isPhoneNumberInput && !isTimePickerInput) {
|
|
264
|
-
|
|
84
|
+
// set the error message element
|
|
85
|
+
const errorMessageContainer = this.errorMessageContainer
|
|
265
86
|
|
|
266
|
-
|
|
267
|
-
if (!errorMessageElement) {
|
|
268
|
-
const existingEmpty = Array.from(errorParent.querySelectorAll(ERROR_MESSAGE_SELECTOR)).find((el) => {
|
|
269
|
-
return (el.textContent || '').trim() === ''
|
|
270
|
-
})
|
|
271
|
-
if (existingEmpty) {
|
|
272
|
-
errorMessageElement = existingEmpty
|
|
273
|
-
errorMessageElement.setAttribute(FORM_VALIDATION_MESSAGE_ATTR, 'true')
|
|
274
|
-
errorMessageElement.setAttribute(FORM_VALIDATION_MESSAGE_REUSED_ATTR, 'true')
|
|
275
|
-
}
|
|
276
|
-
}
|
|
87
|
+
if (target.dataset.message) target.setCustomValidity(target.dataset.message)
|
|
277
88
|
|
|
278
|
-
|
|
279
|
-
errorMessageElement = this.errorMessageContainer
|
|
280
|
-
errorParent.appendChild(errorMessageElement)
|
|
281
|
-
}
|
|
89
|
+
errorMessageContainer.innerHTML = target.validationMessage
|
|
282
90
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
errorMessageElement.style.display = ""
|
|
91
|
+
// add the error message element to the dom tree
|
|
92
|
+
parentElement.appendChild(errorMessageContainer)
|
|
286
93
|
}
|
|
287
94
|
}
|
|
288
95
|
|
|
289
|
-
clearError(target
|
|
290
|
-
const {
|
|
96
|
+
clearError(target) {
|
|
97
|
+
const { parentElement } = target
|
|
98
|
+
const kitElement = parentElement.closest(KIT_SELECTOR)
|
|
99
|
+
// Remove error class from kit element
|
|
291
100
|
if (kitElement) kitElement.classList.remove('error')
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
const reused = messageEl.getAttribute(FORM_VALIDATION_MESSAGE_REUSED_ATTR) === 'true'
|
|
296
|
-
if (reused) {
|
|
297
|
-
messageEl.textContent = ''
|
|
298
|
-
messageEl.style.display = "none"
|
|
299
|
-
messageEl.removeAttribute(FORM_VALIDATION_MESSAGE_ATTR)
|
|
300
|
-
messageEl.removeAttribute(FORM_VALIDATION_MESSAGE_REUSED_ATTR)
|
|
301
|
-
} else {
|
|
302
|
-
messageEl.remove()
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
getErrorParent(target, kitElement, parentElement) {
|
|
308
|
-
const candidate =
|
|
309
|
-
target.closest('.text_input_wrapper') ||
|
|
310
|
-
target.closest('.pb_select_kit_wrapper') ||
|
|
311
|
-
target.closest('.dropdown_wrapper') ||
|
|
312
|
-
target.closest('.input_wrapper') ||
|
|
313
|
-
target.closest('.pb_typeahead_wrapper') ||
|
|
314
|
-
kitElement ||
|
|
315
|
-
parentElement
|
|
316
|
-
|
|
317
|
-
if (kitElement && candidate && candidate !== kitElement && !kitElement.contains(candidate)) {
|
|
318
|
-
return kitElement
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return candidate
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
getControlWrapper(target, kitElement) {
|
|
325
|
-
return (
|
|
326
|
-
target.closest('.dropdown_wrapper') ||
|
|
327
|
-
target.closest('.pb_select_kit_wrapper') ||
|
|
328
|
-
target.closest('.text_input_wrapper') ||
|
|
329
|
-
kitElement?.querySelector?.('.dropdown_wrapper') ||
|
|
330
|
-
kitElement?.querySelector?.('.pb_select_kit_wrapper') ||
|
|
331
|
-
null
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
isReactTypeaheadField(el) {
|
|
336
|
-
return !!(
|
|
337
|
-
el?.closest?.('[data-pb-react-component="Typeahead"]') ||
|
|
338
|
-
el?.closest?.('.pb_typeahead_kit.react-select')
|
|
339
|
-
)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
isTypeaheadField(el) {
|
|
343
|
-
return !!el?.closest?.('.pb_typeahead_kit')
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
getKitElement(target) {
|
|
347
|
-
return (
|
|
348
|
-
target.closest(KIT_SELECTOR) ||
|
|
349
|
-
target.parentElement?.closest(KIT_SELECTOR) ||
|
|
350
|
-
// Some kits don't expose a *_kit class but do expose data hooks.
|
|
351
|
-
target.closest('[data-pb-select]') ||
|
|
352
|
-
target.closest('[data-pb-date-picker]') ||
|
|
353
|
-
target.closest('[data-pb-typeahead-kit]') ||
|
|
354
|
-
null
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
getValidationMessage(target, kitElement) {
|
|
359
|
-
const fromTarget = target.dataset?.message || target.dataset?.validationMessage
|
|
360
|
-
|
|
361
|
-
const wrapperWithMessage =
|
|
362
|
-
target.closest?.('[data-validation-message]') ||
|
|
363
|
-
target.closest?.('[data-pb-select]') ||
|
|
364
|
-
target.closest?.('.dropdown_wrapper') ||
|
|
365
|
-
target.closest?.('.pb_select_kit_wrapper')
|
|
366
|
-
const fromWrapper = wrapperWithMessage?.dataset?.validationMessage
|
|
367
|
-
|
|
368
|
-
const fromKit = kitElement?.dataset?.validationMessage
|
|
369
|
-
|
|
370
|
-
return fromTarget || fromWrapper || fromKit || ''
|
|
101
|
+
// Remove error message from parent element
|
|
102
|
+
const errorMessageContainer = parentElement.querySelector(ERROR_MESSAGE_SELECTOR)
|
|
103
|
+
if (errorMessageContainer) errorMessageContainer.remove()
|
|
371
104
|
}
|
|
372
105
|
|
|
106
|
+
// Check if there are phone number input errors
|
|
373
107
|
hasPhoneNumberValidationErrors() {
|
|
374
108
|
const phoneNumberErrors = this.element.querySelectorAll(PHONE_NUMBER_VALIDATION_ERROR_SELECTOR)
|
|
375
109
|
return phoneNumberErrors.length > 0
|
|
@@ -379,25 +113,12 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
379
113
|
const errorContainer = document.createElement('div')
|
|
380
114
|
const kitClassName = ERROR_MESSAGE_SELECTOR.replace(/\./, '')
|
|
381
115
|
errorContainer.classList.add(kitClassName)
|
|
382
|
-
errorContainer.setAttribute(FORM_VALIDATION_MESSAGE_ATTR, 'true')
|
|
383
|
-
errorContainer.setAttribute('role', 'alert')
|
|
384
|
-
errorContainer.setAttribute('aria-live', 'polite')
|
|
385
116
|
return errorContainer
|
|
386
117
|
}
|
|
387
118
|
get formValidationFields() {
|
|
388
|
-
return this.
|
|
119
|
+
return this._formValidationFields =
|
|
120
|
+
this._formValidationFields || this.element.querySelectorAll(REQUIRED_FIELDS_SELECTOR)
|
|
389
121
|
}
|
|
390
122
|
}
|
|
391
123
|
|
|
392
|
-
window.PbFormValidation = PbFormValidation
|
|
393
|
-
|
|
394
|
-
const __pbStartFormValidation = () => {
|
|
395
|
-
if (!window.PbFormValidation || typeof window.PbFormValidation.start !== 'function') return
|
|
396
|
-
if (window.__pbFormValidationStarted) return
|
|
397
|
-
window.__pbFormValidationStarted = true
|
|
398
|
-
window.PbFormValidation.start()
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
document.addEventListener('DOMContentLoaded', __pbStartFormValidation)
|
|
402
|
-
document.addEventListener('turbo:load', __pbStartFormValidation)
|
|
403
|
-
document.addEventListener('turbo:render', __pbStartFormValidation)
|
|
124
|
+
window.PbFormValidation = PbFormValidation
|
|
@@ -3,6 +3,7 @@ import classnames from 'classnames'
|
|
|
3
3
|
import { buildAriaProps, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
4
4
|
import { GlobalProps, globalProps } from '../utilities/globalProps'
|
|
5
5
|
import { isValidEmoji } from '../utilities/validEmojiChecker'
|
|
6
|
+
import { warnFontAwesomeFallback } from '../utilities/iconFallbackWarning'
|
|
6
7
|
|
|
7
8
|
export type IconSizes = "lg"
|
|
8
9
|
| "xs"
|
|
@@ -164,6 +165,9 @@ const Icon = (props: IconProps) => {
|
|
|
164
165
|
iconElement = <PowerIcon /> as ReactSVGElement
|
|
165
166
|
} else {
|
|
166
167
|
faClasses[`fa-${icon}`] = icon as string
|
|
168
|
+
if (icon && window.PB_ICONS && Object.keys(window.PB_ICONS).length > 0) {
|
|
169
|
+
warnFontAwesomeFallback(icon as string)
|
|
170
|
+
}
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -48,6 +48,29 @@ module Playbook
|
|
|
48
48
|
emoji_regex.match?(icon)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
def warn_font_awesome_fallback
|
|
52
|
+
return "".html_safe if Rails.env.test? || Rails.env.production?
|
|
53
|
+
return "".html_safe if icon.nil? || icon.to_s.empty?
|
|
54
|
+
|
|
55
|
+
escaped_icon = icon.to_s.gsub("'", "\\\\'")
|
|
56
|
+
message = "[Playbook] Icon '#{escaped_icon}' not found in Playbook icons. Falling back to Font Awesome. Font Awesome will be removed from Nitro in the future. Please use Playbook Icons instead. See https://playbook.powerapp.cloud/playbook_icons for available icons."
|
|
57
|
+
|
|
58
|
+
script = "<script type=\"text/javascript\">\n"
|
|
59
|
+
script += "(function() {\n"
|
|
60
|
+
script += " var hostname = window.location.hostname;\n"
|
|
61
|
+
script += " var isLocalDev = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local') || hostname.includes('local.') || !hostname;\n"
|
|
62
|
+
script += " if (!isLocalDev) return;\n"
|
|
63
|
+
script += " if (!window.__PB_WARNED_ICONS__) window.__PB_WARNED_ICONS__ = new Set();\n"
|
|
64
|
+
script += " if (!window.__PB_WARNED_ICONS__.has('#{escaped_icon}')) {\n"
|
|
65
|
+
script += " window.__PB_WARNED_ICONS__.add('#{escaped_icon}');\n"
|
|
66
|
+
script += " console.warn('#{message}');\n"
|
|
67
|
+
script += " }\n"
|
|
68
|
+
script += "})();\n"
|
|
69
|
+
script += "</script>"
|
|
70
|
+
|
|
71
|
+
script.html_safe
|
|
72
|
+
end
|
|
73
|
+
|
|
51
74
|
def classname
|
|
52
75
|
generate_classname(
|
|
53
76
|
"pb_icon_kit",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks which icons have already logged fallback warnings in this session
|
|
3
|
+
* to ensure we only log once per page load per icon
|
|
4
|
+
*/
|
|
5
|
+
const warnedIcons = new Set<string>()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logs a warning when a Playbook icon falls back to Font Awesome
|
|
9
|
+
* - Only logs once per icon per page load (prevents spam on re-renders)
|
|
10
|
+
* - Only logs in local development (not in production, QA, or test environments)
|
|
11
|
+
*
|
|
12
|
+
* @param iconName - The name of the icon that wasn't found in Playbook icons
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* if (!PowerIcon) {
|
|
16
|
+
* warnFontAwesomeFallback('my-icon')
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export const warnFontAwesomeFallback = (iconName: string): void => {
|
|
20
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
const hostname = window.location?.hostname
|
|
26
|
+
const isLocalDev = hostname === 'localhost' ||
|
|
27
|
+
hostname === '127.0.0.1' ||
|
|
28
|
+
hostname?.endsWith('.local') ||
|
|
29
|
+
hostname?.includes('local.') ||
|
|
30
|
+
!hostname
|
|
31
|
+
|
|
32
|
+
if (!isLocalDev) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (warnedIcons.has(iconName)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
warnedIcons.add(iconName)
|
|
42
|
+
|
|
43
|
+
console.warn(
|
|
44
|
+
`[Playbook] Icon '${iconName}' not found in Playbook icons. ` +
|
|
45
|
+
`Falling back to Font Awesome. ` +
|
|
46
|
+
`Font Awesome will be removed from Nitro in the future. Please use Playbook Icons instead. See https://playbook.powerapp.cloud/playbook_icons for available icons.`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resets the warned icons tracker (useful for testing)
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export const resetIconFallbackWarnings = (): void => {
|
|
55
|
+
warnedIcons.clear()
|
|
56
|
+
}
|