index.blade.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. @extends('layouts.app')
  2. @section('content')
  3. <div class="row mb-2">
  4. <div class="col-md-6">
  5. <h3>Ответственные</h3>
  6. </div>
  7. <div class="col-md-6 text-end">
  8. <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
  9. Добавить
  10. </button>
  11. </div>
  12. </div>
  13. @include('partials.table', [
  14. 'id' => $id,
  15. 'header' => $header,
  16. 'strings' => $responsibles,
  17. 'routeName' => 'responsible.show',
  18. 'routeParam' => 'responsible',
  19. ])
  20. @include('partials.pagination', ['items' => $responsibles])
  21. <!-- Модальное окно добавления-->
  22. <div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
  23. <div class="modal-dialog modal-fullscreen-sm-down modal-lg">
  24. <div class="modal-content">
  25. <div class="modal-header">
  26. <h1 class="modal-title fs-5" id="addModalLabel">Добавить ответственного</h1>
  27. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
  28. </div>
  29. <div class="modal-body">
  30. <form action="{{ route('responsible.store') }}" method="post">
  31. @csrf
  32. <div class="row mb-2">
  33. <label for="area_search" class="col-form-label small col-md-4 text-md-end">
  34. Район
  35. <sup>*</sup>
  36. </label>
  37. <div class="col-md-8">
  38. <div class="position-relative">
  39. <input type="text"
  40. class="form-control form-control-sm"
  41. id="area_search"
  42. placeholder="Введите район..."
  43. autocomplete="off"
  44. required>
  45. <input type="hidden" name="area_id" id="area_id" value="{{ old('area_id', '') }}" required>
  46. <div class="autocomplete-dropdown autocomplete-dropdown--order" id="area_dropdown"></div>
  47. </div>
  48. <div class="form-text" id="area_hint"></div>
  49. </div>
  50. </div>
  51. @include('partials.input', ['name' => 'name', 'title' => 'ФИО', 'required' => true])
  52. @include('partials.input', ['name' => 'phone', 'title' => 'Телефон', 'required' => true])
  53. @include('partials.input', ['name' => 'post', 'title' => 'Должность'])
  54. @include('partials.submit', ['name' => 'Добавить'])
  55. </form>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. @endsection
  61. @push('scripts')
  62. <script>
  63. waitForJQuery(function () {
  64. const $modal = $('#addModal');
  65. const $input = $modal.find('#area_search');
  66. const $dropdown = $modal.find('#area_dropdown');
  67. const $hiddenInput = $modal.find('#area_id');
  68. const $hint = $modal.find('#area_hint');
  69. const $form = $modal.find('form');
  70. let currentFocus = -1;
  71. const areas = @json($areasForSearch ?? []);
  72. function escapeHtml(text) {
  73. return String(text)
  74. .replace(/&/g, '&amp;')
  75. .replace(/</g, '&lt;')
  76. .replace(/>/g, '&gt;')
  77. .replace(/"/g, '&quot;')
  78. .replace(/'/g, '&#039;');
  79. }
  80. function escapeRegex(str) {
  81. return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  82. }
  83. function highlightMatch(text, query) {
  84. if (!text || !query) {
  85. return escapeHtml(text || '');
  86. }
  87. const safeText = escapeHtml(text);
  88. const regex = new RegExp('(' + escapeRegex(query) + ')', 'gi');
  89. return safeText.replace(regex, '<strong>$1</strong>');
  90. }
  91. function showDropdown(items, query) {
  92. $dropdown.empty();
  93. currentFocus = -1;
  94. if (!items.length) {
  95. $dropdown.html('<div class="autocomplete-item text-muted">Ничего не найдено</div>');
  96. $dropdown.show();
  97. return;
  98. }
  99. items.forEach(function (item) {
  100. const html = '<div class="autocomplete-item" data-id="' + item.id + '" data-name="' + escapeHtml(item.name) + '">' +
  101. '<div class="article">' + highlightMatch(item.name, query) + '</div>' +
  102. '</div>';
  103. $dropdown.append(html);
  104. });
  105. $dropdown.show();
  106. }
  107. function hideDropdown() {
  108. $dropdown.hide();
  109. }
  110. function selectItem($item) {
  111. const id = String($item.data('id') || '');
  112. const name = String($item.data('name') || '');
  113. $input.val(name);
  114. $hiddenInput.val(id);
  115. $hint.html('<i class="bi bi-check-circle text-success"></i> Выбрано: ' + name);
  116. $input.removeClass('is-invalid');
  117. hideDropdown();
  118. }
  119. function setActive($items) {
  120. $items.removeClass('active');
  121. if (currentFocus >= 0 && currentFocus < $items.length) {
  122. const $active = $items.eq(currentFocus);
  123. $active.addClass('active');
  124. $active[0].scrollIntoView({block: 'nearest'});
  125. }
  126. }
  127. function searchAreas(query) {
  128. const normalized = String(query || '').trim().toLowerCase();
  129. if (normalized.length < 1) {
  130. hideDropdown();
  131. return;
  132. }
  133. const items = areas.filter(function (item) {
  134. return String(item.name).toLowerCase().includes(normalized);
  135. });
  136. showDropdown(items.slice(0, 50), normalized);
  137. }
  138. $input.on('input', function () {
  139. $hiddenInput.val('');
  140. $hint.html('');
  141. $input.removeClass('is-invalid');
  142. searchAreas($(this).val());
  143. });
  144. $input.on('focus', function () {
  145. if (!$hiddenInput.val()) {
  146. searchAreas($(this).val());
  147. }
  148. });
  149. $input.on('keydown', function (e) {
  150. const $items = $dropdown.find('.autocomplete-item:not(.text-muted)');
  151. if (e.key === 'ArrowDown') {
  152. e.preventDefault();
  153. currentFocus++;
  154. if (currentFocus >= $items.length) {
  155. currentFocus = 0;
  156. }
  157. setActive($items);
  158. } else if (e.key === 'ArrowUp') {
  159. e.preventDefault();
  160. currentFocus--;
  161. if (currentFocus < 0) {
  162. currentFocus = $items.length - 1;
  163. }
  164. setActive($items);
  165. } else if (e.key === 'Enter') {
  166. if ($items.length > 0 && currentFocus > -1) {
  167. e.preventDefault();
  168. selectItem($items.eq(currentFocus));
  169. }
  170. } else if (e.key === 'Escape') {
  171. hideDropdown();
  172. }
  173. });
  174. $dropdown.on('click', '.autocomplete-item:not(.text-muted)', function () {
  175. selectItem($(this));
  176. });
  177. $(document).on('click', function (e) {
  178. if (!$(e.target).closest('#area_search, #area_dropdown').length) {
  179. hideDropdown();
  180. }
  181. });
  182. $form.on('submit', function (e) {
  183. if (!$hiddenInput.val()) {
  184. e.preventDefault();
  185. $input.addClass('is-invalid').focus();
  186. $hint.html('<span class="text-danger">Выберите район из списка</span>');
  187. }
  188. });
  189. @if(old('area_id'))
  190. const oldAreaId = '{{ old('area_id') }}';
  191. const oldArea = areas.find(function (item) {
  192. return String(item.id) === String(oldAreaId);
  193. });
  194. if (oldArea) {
  195. $input.val(oldArea.name);
  196. $hint.html('<i class="bi bi-check-circle text-success"></i> Выбрано: ' + oldArea.name);
  197. }
  198. @endif
  199. $modal.on('hidden.bs.modal', function () {
  200. hideDropdown();
  201. });
  202. });
  203. </script>
  204. @endpush