|
63 | 63 | function wire(v) { |
64 | 64 | const groups = discoverGroups(); |
65 | 65 |
|
| 66 | + // Make jQuery Validate prefer element.id over name (fixes accented name issues) |
| 67 | + v.idOrName = function (el) { |
| 68 | + return el.id || (el.name ? el.name.replace(/[^\w\-]+/g, '_') : ''); |
| 69 | + }; |
| 70 | + |
66 | 71 | // 1) Register one jQuery Validate "group" per checkbox base. |
67 | 72 | // This makes jQuery Validate treat multiple field names as one logical group |
68 | 73 | // when deciding which single label/entry to render. |
|
72 | 77 | v.settings.groups[base] = groups[base].join(' '); |
73 | 78 | }); |
74 | 79 |
|
75 | | - // Precompute the "first" field name for each base; duplicates will be suppressed. |
76 | | - const firstOf = {}; |
| 80 | + // Precompute the FIRST field *id* of each base so we can place a single inline label. |
| 81 | + const firstIdOf = {}; |
77 | 82 | Object.keys(groups).forEach((base) => { |
78 | | - firstOf[base] = groups[base][0]; |
| 83 | + const firstName = groups[base][0]; |
| 84 | + const $el = $form.find('[name="' + CSS.escape(firstName) + '"]').first(); |
| 85 | + firstIdOf[base] = $el.attr('id') || ''; |
79 | 86 | }); |
80 | 87 |
|
81 | 88 | // Shadow a local baseOf so inner functions have it in scope (performance/readability). |
|
90 | 97 |
|
91 | 98 | // If this is part of a checkbox base and it's NOT the first item, |
92 | 99 | // skip placing the label entirely (the "first" item will get it). |
93 | | - if (base && name !== firstOf[base]) { |
94 | | - return; |
| 100 | + if (base) { |
| 101 | + const id = element.attr('id') || ''; |
| 102 | + // Only place for the FIRST id in the group, skip others to avoid duplicates. |
| 103 | + if (firstIdOf[base] && id !== firstIdOf[base]) return; |
95 | 104 | } |
96 | 105 | return origPlace(error, element); |
97 | 106 | }; |
|
104 | 113 | const seen = new Set(); |
105 | 114 | const list = []; |
106 | 115 | const map = {}; |
| 116 | + |
107 | 117 | validator.errorList.forEach((item) => { |
108 | | - const nm = item.element && item.element.name; |
109 | | - // Fall back to name for non-array fields. |
| 118 | + const el = item.element; |
| 119 | + const nm = el && el.name; |
| 120 | + const id = el && el.id; |
110 | 121 | const base = baseOf(nm) || nm; |
| 122 | + const dedupeKey = base + '::' + (id || ''); |
111 | 123 |
|
112 | | - // Keep the first error per base, drop the rest. |
113 | | - if (!seen.has(base)) { |
114 | | - seen.add(base); |
| 124 | + if (!seen.has(dedupeKey)) { |
| 125 | + seen.add(dedupeKey); |
115 | 126 | list.push(item); |
116 | 127 | if (nm) map[nm] = item.message; |
117 | 128 | } |
|
121 | 132 | validator.errorList = list; |
122 | 133 | validator.errorMap = map; |
123 | 134 |
|
| 135 | + // Clean up any duplicate labels that may exist (caused earlier by accented names) |
| 136 | + const seenLabelFor = {}; |
| 137 | + $(formEl).find('label.error[for]').each(function () { |
| 138 | + const f = $(this).attr('for'); |
| 139 | + if (seenLabelFor[f]) $(this).remove(); |
| 140 | + else seenLabelFor[f] = true; |
| 141 | + }); |
| 142 | + |
124 | 143 | // Allow any original invalidHandler (including WET's) to run with the reduced list. |
125 | 144 | return origInvalid.call(this, formEl, validator); |
126 | 145 | }; |
|
0 commit comments