1// Copyright (c) 2021-2025 GrayBits LLC 2 3import * as Builder from 'gray_matter/utility/builder' 4import * as Accessibility from 'gray_matter/utility/accessibility' 5 6export default class Errors{ 7 constructor(form, form_errors = false){ 8 this.form = form; 9 this.form_errors = form_errors || this.form.querySelector('.form_errors'); 10 this.current_error_elements = []; 11 } 12 show(data, no_scroll){ 13 this.current_error_elements = []; 14 this.current_error_index = 0; 15 this.form.classList.remove('with_errors'); 16 17 if (data.errors) this.showAttributeErrors(data.errors); 18 this.showCellErrors(data.cell_errors); 19 20 if (data.message && this.form_errors){ 21 this.form_errors.innerHTML = data.message; 22 this.form_errors.classList.add('shown'); 23 } 24 if (this.form.classList.contains('item_form') && 25 this.current_error_elements.length > 0){ 26 if (this.current_error_elements.length > 1){ 27 let next_error = Builder.createElement('button', {class: ['button', 'next', 'icon_only', 'error']}); 28 let previous_error = Builder.createElement('button', {class: ['button', 'previous', 'icon_only', 'error']}); 29 let errors_nav = document.getElementById('errors_navigation'); 30 errors_nav.innerHTML = '<span id="errors_nav_text">Jump to error</span>'; 31 errors_nav.append(previous_error); 32 errors_nav.append(next_error) 33 errors_nav.classList.add('shown'); 34 previous_error.addEventListener('click', (e) => this.showError(-1)); 35 next_error.addEventListener('click', (e) => this.showError(1)); 36 } 37 this.showCurrentError(no_scroll); 38 } 39 } 40 41 showError(direction){ 42 this.current_error_index = (this.current_error_index + direction + this.current_error_elements.length) % this.current_error_elements.length; 43 this.showCurrentError(); 44 } 45 46 showCurrentError(no_scroll){ 47 let tab_navigation = document.querySelector('.tab_navigation'); 48 let error_element = this.current_error_elements[this.current_error_index]; 49 let in_tab = error_element.closest('.tab'); 50 if (in_tab){ 51 in_tab.openTab(); 52 } 53 if (!no_scroll) error_element.scrollIntoView(); 54 Accessibility.focus(error_element); 55 56 if (tab_navigation){ 57 let scrollable = tab_navigation.closest('.scrollable'); 58 scrollable.scrollTop = scrollable.scrollTop - tab_navigation.offsetHeight; 59 } 60 } 61 62 showAttributeErrors(errors, options = {}){ 63 for (const [attribute, error] of Object.entries(errors)){ 64 this.showAttributeError(attribute, error, options); 65 } 66 this.form.classList.add('with_errors'); 67 } 68 69 showCellErrors(cell_errors, options = {}){ 70 if (!cell_errors) return; 71 72 let has_errors = false; 73 if (cell_errors.cell_groups){ 74 for (const [cell_group, errors] of Object.entries(cell_errors.cell_groups)){ 75 this.showCellGroupErrors(this.cellsForErrors(cell_group, options), errors, options); 76 has_errors = true; 77 } 78 } 79 80 has_errors = has_errors || this.showNestedBelongsToErrors(cell_errors.nested_belongs_to); 81 has_errors = has_errors || this.showMediaSetErrors(cell_errors.media_sets, document, options); 82 83 if (has_errors){ 84 this.form.classList.add('with_errors'); 85 } 86 } 87 88 cellsForErrors(cell_group, options){ 89 let base_selector = `.field_wrapper[data-attribute="${cell_group}"]` 90 if (options.nested_belongs_to){ 91 return document.querySelector(`${base_selector}[data-belongs-to-attribute="${options.nested_belongs_to}"] .cells`); 92 } else { 93 return document.querySelector(`${base_selector}:not(data-belongs-to-attribute) .cells`); 94 } 95 } 96 97 showNestedBelongsToErrors(nested_belongs_to_errors){ 98 if (!nested_belongs_to_errors) return; 99 100 for (const [attribute, errors] of Object.entries(nested_belongs_to_errors)){ 101 if (errors.errors) this.showAttributeErrors(errors.errors, { nested_belongs_to: attribute }); 102 this.showCellErrors(errors.cell_errors, { nested_belongs_to: attribute }); 103 } 104 } 105 106 showMediaSetErrors(media_set_errors, parent, options){ 107 if(!media_set_errors) return; 108 109 let has_errors = false;
110 for (const [media_set, errors] of Object.entries(media_set_errors)){ 111 let media_cells = parent.querySelector(`.field_wrapper[data-attribute="${media_set}"] .media_cells`); 112 for (const [media_cell_position, media_cell_errors] of Object.entries(errors)){ 113 let media_cell = media_cells.children[media_cell_position * 1]; 114 this.showAttributeErrors(media_cell_errors, { cell: media_cell }); 115 has_errors = true; 116 } 117 } 118 return has_errors; 119 } 120 121 showCellGroupErrors(cells, errors, options = {}){ 122 for (const [cell_position, cell_errors] of Object.entries(errors)){ 123 let cell = cells.children[cell_position * 1]; 124 this.showAttributeErrors(cell_errors.errors, { cell: cell, nested_belongs_to: options.nested_belongs_to }); 125 this.showMediaSetErrors(cell_errors.media_set_errors, cell, options); 126 } 127 } 128 129 showAttributeError(attribute, errors, options = {}){ 130 let field_wrapper = this.getFieldWrapper(attribute, options); 131 132 if (!field_wrapper){ 133 return; 134 } 135 let label = field_wrapper.querySelector('label'); 136 let error_wrapper = field_wrapper.querySelector('.attribute_errors'); 137 if (!error_wrapper){ 138 error_wrapper = Builder.createElement('div', { class: ['attribute_errors', 'errors'] }); 139 if (label){ 140 Builder.insertAfter(label, error_wrapper); 141 } else { 142 field_wrapper.innerHTML = ''; 143 field_wrapper.appendChild(error_wrapper); 144 } 145 } 146 this.current_error_elements.push(field_wrapper); 147 error_wrapper.innerHTML = Array.isArray(errors) ? errors.join(', ') : errors; 148 field_wrapper.classList.add('field_with_errors'); 149 } 150 151 152 getFieldWrapper(attribute, options){ 153 if (attribute == 'base'){ 154 return document.getElementById('base_error'); 155 } else { 156 let base_selector = '.field_wrapper' 157 if (options.nested_belongs_to){ 158 if (options.cell){ 159 base_selector += `[data-belongs-to-attribute="${options.nested_belongs_to}"] .field_wrapper`; 160 } else { 161 base_selector += `[data-belongs-to-attribute="${options.nested_belongs_to}"]`; 162 } 163 } 164 if (options.cell){ 165 return options.cell.querySelector(`${base_selector}[data-attribute="${attribute}"]`); 166 } else { 167 return this.form.querySelector(`${base_selector}[data-attribute="${attribute.replace(/_cells$/g,'')}"]:not(.cell_field)`); 168 } 169 } 170 } 171 172 clear(){ 173 if (this.form_errors){ 174 this.form_errors.classList.remove('shown'); 175 } 176 if (this.form_errors){ 177 this.form_errors.innerHTML = ''; 178 } 179 let errors_nav = document.getElementById('errors_navigation'); 180 if (errors_nav){ 181 errors_nav.classList.remove('shown', 'single'); 182 errors_nav.innerHTML = ''; 183 } 184 this.form.querySelectorAll('.field_with_errors').forEach((e) => { 185 e.classList.remove('field_with_errors'); 186 let error_message = e.querySelector('.attribute_errors'); 187 if (error_message){ 188 error_message.remove(); 189 } 190 }); 191 } 192} 193;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.