PageSourceSearch

https://umetex.com/wa-data/public/site/themes/umetex/js/main.js?v1.0.39

js umetex.com collected 2026-10-09 23:11:46 UTC 34,046 bytes, 1,323 lines download raw bytes

1const createElMarker = (
2  icon = "/wa-apps/site/themes/umetex/icons/geomap/marker.svg",
3  size = 40,
4) => {
5  const el = document.createElement("img");
6
7  el.src = icon;
8  el.width = size;
9  el.height = size;
10
11  return el;
12};
13
14const iconMarker = "/wa-apps/site/themes/umetex/icons/geomap/marker.svg";
15const iconMarker2 = "/wa-apps/site/themes/umetex/icons/geomap/marker-1.svg";
16
17const mapBounds = {
18  minLat: 40,
19  maxLat: 85,
20  minLng: 20,
21  maxLng: 180,
22};
23
24function latLngToPixel(lat, lng, mapContainer, mapWidth, mapBounds) {
25  if (
26    lat > mapBounds.maxLat ||
27    lat < mapBounds.minLat ||
28    lng > mapBounds.maxLng ||
29    lng < mapBounds.minLng
30  ) {
31    console.warn("the point is not within the boundaries");
32    return;
33  }
34
35  const mapBox = mapContainer.getBoundingClientRect();
36
37  if (!mapBox) {
38    console.error("undefined mapBox");
39    return;
40  }
41  const mapLowerY = mapBox.bottom + window.scrollY;
42  const mapLimitX = mapBox.right;
43  const mapLowerYPercent = (100 * mapLowerY) / document.body.clientHeight;
44  const mapLimitXPercent = (100 * mapLimitX) / window.innerWidth;
45
46  const pointLowerYPercent = (mapLowerYPercent * lat) / mapBounds.maxLat;
47  const pointLimitXPercent = (mapLimitXPercent * lng) / mapBounds.maxLng;
48
49  return { x: pointLimitXPercent, y: pointLowerYPercent };
50}
51
52const initRegionsMap = (data) => {
53  if (window.innerWidth < 1024) return;
54
55  document.body.style.position = "relative";
56
57  const mapContainer = document.querySelector(".geomap-container");
58  if (!mapContainer) {
59    console.error("Контейнер карты не найден!");
60    return;
61  }
62  const mapWidth = mapContainer.offsetWidth;
63
64  data.forEach((region) => {
65    const { coords, type } = region;
66
67    if (!coords) {
68      console.warn(`Нет координат для региона id: ${regionData.id}`);
69      return;
70    }
71
72    const [lat, lng] = coords.split(",").map(parseFloat);
73
74    const pixelCoords = latLngToPixel(
75      lat,
76      lng,
77      mapContainer,
78      mapWidth,
79      mapBounds,
80    );
81
82    const marker = createElMarker(
83      type === "service" ? iconMarker2 : iconMarker,
84    );
85    marker.dataset.name = region.id;
86
87    const container = document.createElement("div");
88    container.style.position = "absolute";
89    container.style.top = pixelCoords.y + "%";
90    container.style.left = pixelCoords.x + "%";
91    container.style.transform = "translateX(50%) translateY(50%)";
92    container.style.cursor = "pointer";
93    container.style.zIndex = "10";
94
95    container.append(marker);
96
97    (() => {
98      if (Object.keys(region?.data ? region.data : {}).length === 0) return;
99
100      const tooltip = document
101        .querySelector("div[data-card-ref='geomapMarkerTooltip']")
102        ?.cloneNode(true);
103
104      if (!tooltip) return;
105
106      try {
107        const elCardName = tooltip.querySelector("[data-card-ref='cardName']");
108        const elContactsTitle = tooltip.querySelector(
109          "[data-card-ref='contactsTitle']",
110        );
111        const elRowTel = tooltip.querySelector("[data-card-ref='rowTel']");
112        const elTel = tooltip.querySelector("[data-card-ref='tel']");
113        const elRowEmail = tooltip.querySelector("[data-card-ref='rowEmail']");
114        const elEmail = tooltip.querySelector("[data-card-ref='email']");
115        const elRowWork = tooltip.querySelector("[data-card-ref='rowWork']");
116        const elWork = tooltip.querySelector("[data-card-ref='work']");
117        const elSiteLink = tooltip.querySelector("[data-card-ref='siteLink']");
118
119        elCardName.innerText = region.data.cardName;
120        elContactsTitle.innerText = region.data.contactsTitle;
121        elTel.innerText = region.data.tel;
122        elEmail.innerText = region.data.email;
123        elWork.innerText = region.data.work;
124        elSiteLink.href = region.data.siteLink;
125      } catch (e) {
126        console.error(e);
127        return;
128      }
129
130      tooltip.style.opacity = "0%";
131      tooltip.classList.remove("hidden");
132
133      container.addEventListener(
134        "mouseenter",
135        () => (tooltip.style.opacity = "100%"),
136      );
137      container.addEventListener(
138        "mouseleave",
139        () => (tooltip.style.opacity = "0%"),
140      );
141
142      container.append(tooltip);
143    })();
144
145    document.body.append(container);
146  });
147};
148
149document.addEventListener("alpine:init", (qualifiedName, value) => {
150  Alpine.data("page", () => ({
151    navs: {
152      isShow: false,
153      isOpen: false,
154      data: [],
155
156      init() {
157        const navElArr = [...document.querySelectorAll("[data-nav-name]")];
158
159        if (!navElArr.length > 1) return;
160
161        this.isShow = true;
162        this.data = navElArr.map((el) => ({
163          link: el.id,
164          name: el.dataset.navName,
165        }));
166      },
167
168      toggle() {
169        this.isOpen = !this.isOpen;
170      },
171      close() {
172        this.isOpen = false;
173      },
174      move: {
175        ["x-on:click"](e) {
176          e.preventDefault();
177
178          const id = e.target.getAttribute("href");
179
180          document.querySelector("#" + id).scrollIntoView({
181            behavior: "smooth",
182            block: "start",
183            inline: "nearest",
184          });
185        },
186      },
187      moveToFeedback: {
188        ["x-on:click"](e) {
189          e.preventDefault();
190
191          document.querySelector("#feedback").scrollIntoView({
192            behavior: "smooth",
193            blocks: "start",
194            inline: "nearest",
195          });
196        },
197      },
198    },
199    modals: {
200      active: "",
201      isOpen: false,
202
203      init($el, $refs) {
204        this.el = $el;
205        this.$refs = $refs;
206
207        console.log("--modals init", this.$refs);
208      },
209
210      open(name = "") {
211        if (!name || !this.$refs[name]) {
212          this.close();
213          return;
214        }
215
216        if (this.active) this.close();
217
218        this.isOpen = true;
219        this.active = name;
220
221        console.log("--modal open", this.active, this.isOpen);
222      },
223
224      close() {
225        this.active = "";
226        this.isOpen = false;
227
228        console.log("--modal close", this.active, this.isOpen);
229      },
230    },
231
232    init() {
233      this.navs.init();
234      this.modals.init(this.$el, this.$refs);
235    },
236
237    formatDate(
238      date = "",
239      options = { day: "numeric", month: "long", year: "numeric" },
240    ) {
241      return new Date(date).toLocaleString("ru", options).replace(" г.", "");
242    },
243  }));
244  Alpine.data(
245    "combobox",
246    (
247      data = {
248        options: [],
249      },
250      isSort = false,
251      isSearch = false,
252    ) => ({
253      isOpen: false,
254      selectedOption: null,
255      openedWithKeyboard: false,
256      isSearch: isSearch,
257      searchText: "",
258      parentId: null,
259      ...data,
260
261      init() {
262        // console.log("combobox --init", data);
263        this.el = this.$el;
264
265        if (isSort) {
266          this.options.sort((a, b) => a.label.localeCompare(b.label));
267        }
268        this.renderOptions();
269        this.selectSearchParams();
270      },
271      open() {
272        this.isOpen = true;
273      },
274      toggle() {
275        this.isOpen = !this.isOpen;
276      },
277      close() {
278        this.isOpen = false;
279        this.openedWithKeyboard = false;
280      },
281      select() {
282        this.selectedOption = {
283          label: this.$el.dataset.comboboxOptionLabel,
284          value: this.$el.dataset.comboboxOptionValue,
285        };
286        this.callEvent(
287          this.el.dataset.customTag,
288          this.selectedOption.value,
289          this.selectedOption.value,
290        );
291        this.close();
292      },
293      callEvent(type = null, value = null, label = null, init = false) {
294        this.$refs.container.dispatchEvent(
295          new CustomEvent("combobox", {
296            type: type,
297            detail: {
298              name: this.$refs.container.dataset.comboboxName,
299              value: value,
300              label: label,
301              init: init,
302            },
303          }),
304        );
305      },
306      highlightFirstMatchingOption(pressedKey) {
307        const option = this.options.find((item) =>
308          item.label.toLowerCase().startsWith(pressedKey.toLowerCase()),
309        );
310
311        if (option) {
312          const index = this.options.indexOf(option);
313          const allOptions =
314            this.$refs.comboboxList.querySelectorAll(`.combobox-option`);
315
316          if (allOptions[index]) {
317            allOptions[index].focus();
318          }
319        }
320      },
321      createOptionEl(option) {
322        if (!option || !option.value || !option.label) {
323          console.error("missing props", option);
324          return false;
325        }
326
327        const optionEl = document.createElement("li");
328        optionEl.innerHTML = option.label;
329        optionEl.classList.add(
330          "combobox-option",
331          "cursor-pointer",
332          "px-6",
333          "py-2",
334          "w-full",
335          "transition-all",
336          "hover:bg-grey-light",
337          "focus-visible:bg-grey-light",
338          "focus-visible:outline-none",
339        );
340        optionEl.dataset.comboboxOptionValue = option.value;
341        optionEl.dataset.comboboxOptionLabel = option.label;
342        optionEl.setAttribute(
343          "x-bind:data-combobox-option-select",
344          `selectedOption && selectedOption.value == '${option.value}'`,
345        );
346        optionEl.setAttribute("x-on:click", `select()`);
347        optionEl.setAttribute("x-on:keydown.enter", `select()`);
348        optionEl.setAttribute(
349          "x-show",
350          `'${option.value}'.includes(searchText)`,
351        );
352        optionEl.setAttribute("tabindex", "0");
353
354        return optionEl;
355      },
356      renderOptions() {
357        if (!this.$refs.comboboxList) return;
358
359        this.$refs.comboboxList.innerHTML = "";
360        this.options.forEach((option) => {
361          this.$refs.comboboxList.append(this.createOptionEl(option));
362        });
363      },
364      selectSearchParams() {
365        if (!location) {
366          console.warn("location is not defined");
367          return;
368        }
369        if (!this.parentId) {
370          console.warn("parentId is null");
371          return;
372        }
373
374        const params = new URL(location.href).searchParams;
375        const keyName = this.$refs.container.dataset.comboboxName;
376        const paramObj = JSON.parse(params.get(this.parentId)) || {};
377        const paramValue = paramObj[keyName];
378
379        if (!paramValue) return;
380
381        const selectOption = this.options.find(
382          (item) => item.value === paramValue,
383        );
384
385        this.selectedOption = {
386          label: selectOption.label,
387          value: selectOption.value,
388        };
389        this.callEvent(
390          this.$refs.container.dataset.customTag,
391          selectOption.value,
392          selectOption.label,
393          true,
394        );
395      },
396    }),
397  );
398  Alpine.data("pagination", (data = {}) => ({
399    id: null,
400    count: null,
401    perView: 6,
402    page: 1,
403    pages: 0,
404    showMoreItems: 0,
405    isDesktop: window.innerWidth >= 1280,
406    range: 2,
407    visiblePages: [],
408    contentRangeButton: "...",
409    startShowMore: 0,
410    endShowMore: 0,
411    ...data,
412    isReset: false,
413
414    init() {
415      console.log("pagination init", data);
416      if (!(this.id && this.count)) return;
417
418      if (data.isPagination === "on") {
419        this.isPagination = true;
420      }
421
422      this.pages = Math.ceil(this.count / this.perView);
423      this.isDesktop = window.innerWidth >= 1280;
424      this.updateVisiblePage();
425      this.updateLimitShowMore();
426
427      window.addEventListener("resize", () => {
428        this.isDesktop = window.innerWidth >= 1280;
429      });
430
431      this.$watch("page", () => {
432        // console.log("watch page", this.page);
433        this.updateVisiblePage();
434
435        if (this.isReset) {
436          this.isReset = false;
437          return;
438        }
439        this.updateLimitShowMore();
440        this.paginate();
441      });
442
443      this.$el.addEventListener("paginationReset", (e) => {
444        console.log("paginationReset", e.detail);
445        console.log(this.page, this.pages, this.count, this.visiblePages);
446        if (e.detail?.count) {
447          this.isReset = true;
448          this.count = e.detail.count;
449          this.pages = Math.ceil(this.count / this.perView);
450          this.page = 1;
451        }
452      });
453    },
454    updateVisiblePage() {
455      this.visiblePages = [];
456      if (!this.isDesktop) {
457        return;
458      }
459
460      if (this.pages <= this.range * 2 + 1) {
461        for (let i = 1; i <= this.pages; i++) {
462          this.visiblePages.push(i);
463        }
464        return;
465      }
466
467      const startPage = Math.max(1, this.page - this.range);
468      const endPage = Math.min(this.pages, this.page + this.range);
469
470      if (startPage > 1) {
471        this.visiblePages.push(1);
472        if (startPage > 2) this.visiblePages.push(this.contentRangeButton);
473      }
474
475      for (let i = startPage; i <= endPage; i++) {
476        this.visiblePages.push(i);
477      }
478
479      if (endPage < this.pages) {
480        if (endPage < this.pages - 1) {
481          this.visiblePages.push(this.contentRangeButton);
482        }
483        this.visiblePages.push(this.pages);
484      }
485    },
486    setPage(i = 0) {
487      const page = this.visiblePages[i];
488
489      if (page !== this.contentRangeButton) {
490        this.page = page;
491        return;
492      }
493
494      const prevPage = this.visiblePages[i - 1];
495      const nextPage = this.visiblePages[i + 1];
496
497      if (typeof prevPage !== "number" || typeof nextPage !== "number") {
498        console.error("invalid page, prevPage - nextPage:", prevPage, nextPage);
499        return;
500      }
501
502      this.page = prevPage + Math.round((nextPage - prevPage) / 2);
503    },
504    nextPage() {
505      if (this.page < this.pages) {
506        this.page++;
507      }
508    },
509    prevPage() {
510      if (this.page > 1) {
511        this.page--;
512      }
513    },
514    showMore() {
515      this.startShowMore = (this.page - 1) * this.perView + 1;
516
517      const newEndShowMore = this.endShowMore + parseInt(this.perView);
518      if (newEndShowMore > this.count) {
519        this.endShowMore = this.count;
520      } else {
521        this.endShowMore = newEndShowMore;
522      }
523
524      this.page = 1;
525      this.paginate(true);
526    },
527    updateLimitShowMore() {
528      this.startShowMore = (this.page - 1) * this.perView + 1;
529      const maxItems = this.page * this.perView;
530
531      if (maxItems > this.count) this.endShowMore = this.count;
532      else this.endShowMore = maxItems;
533    },
534    paginate(showMore = false) {
535      const config = {
536        bubbles: true,
537        detail: {
538          id: this.id,
539          page: this.page,
540        },
541      };
542
543      if (showMore) {
544        config.detail.showMore = true;
545        config.detail.startShowMore = this.startShowMore;
546        config.detail.endShowMore = this.endShowMore;
547      }
548
549      this.$el.dispatchEvent(new CustomEvent("paginate", config));
550    },
551  }));
552  Alpine.data("filter", (data = {}) => ({
553    id: null,
554    posts: [],
555    viewPosts: [],
556    perView: 6,
557    isCalendar: false,
558    isPagination: false,
559    ...data,
560    filters: [],
561    page: 1,
562    startShowMore: null,
563    endShowMore: null,
564    showMore: false,
565    paginationEl: null,
566    filterPagination: false,
567
568    init() {
569      if (!this.$el.dataset.filterId) {
570        console.error("The component is missing the data-filter-id attribute.");
571      }
572
573      this.id = this.$el.dataset.filterId;
574
575      if (!this.isPagination) {
576        this.perView = this.posts.length;
577      }
578
579      this.viewPosts = this.posts.slice(0, this.perView);
580      this.el = this.$el;
581
582      this.comboboxListenerInit();
583
584      if (this.isCalendar) {
585        this.calendarListenerInit();
586      }
587
588      if (this.isPagination) {
589        this.paginationEl = this.$el.querySelector(
590          "[data-component='pagination']",
591        );
592        this.paginationInit();
593      }
594    },
595    comboboxListenerInit() {
596      const arr = [...this.el.querySelectorAll("[data-custom-tag='combobox']")];
597
598      if (!arr) return;
599
600      arr.forEach((combobox) =>
601        combobox.addEventListener("combobox", (e) => {
602          if (e.type !== "combobox") return;
603          // console.log("combobox --event", e);
604
605          this.setFilter(e.detail.name, e.detail.value, e.detail?.init);
606        }),
607      );
608    },
609    calendarListenerInit() {
610      const arr = [...this.el.querySelectorAll("[data-custom-tag='d']")];
611
612      if (!arr) return;
613
614      arr.forEach((calendar) =>
615        calendar.addEventListener("calendar", (e) => {
616          // console.log("calendar --event", e);
617
618          this.setFilter(e.detail.name, e.detail.value, e.detail?.init);
619        }),
620      );
621    },
622    setFilter(key = "", value = "", init = false) {
623      const filterIndex = this.filters.findIndex((item) => item.key === key);
624
625      if (filterIndex === -1) {
626        this.filters.push({
627          key: key,
628          value: value,
629        });
630      } else {
631        if (this.filters[filterIndex].value === value) {
632          this.filters.splice(filterIndex, 1);
633        } else {
634          this.filters[filterIndex].value = value;
635        }
636      }
637
638      if (!init) {
639        this.updateSearchParams();
640      }
641      this.updateView();
642    },
643    updateView() {
644      const res = [];
645
646      if (this.filters.length === 0 && !this.isPagination) {
647        this.viewPosts = this.posts;
648        return;
649      }
650
651      this.posts.forEach((post) => {
652        let isInsert = true;
653
654        this.filters.forEach((filter) => {
655          if (filter.value === "all") return;
656          if (!post[filter.key].split(",").includes(filter.value))
657            isInsert = false;
658        });
659
660        if (isInsert) res.push(post);
661      });
662
663      if (!this.filterPagination) {
664        this.paginationUpdate(res.length);
665      }
666      this.filterPagination = false;
667
668      if (this.isPagination) {
669        const paginationPosts = res.length > 0 ? res : this.posts;
670
671        if (this.showMore && this.startShowMore && this.endShowMore) {
672          this.viewPosts = paginationPosts.slice(
673            this.startShowMore - 1,
674            this.endShowMore,
675          );
676          return;
677        }
678
679        this.viewPosts = paginationPosts.slice(
680          (this.page - 1) * this.perView,
681          this.page * this.perView,
682        );
683      } else {
684        this.viewPosts = res;
685      }
686    },
687    updateSearchParams() {
688      // console.log("updateSearchParams", history, this.filters.length);
689      if (!history) return;
690
691      if (this.filters.length === 0) {
692        history.pushState(null, null, `?${this.id}=`);
693
694        return;
695      }
696
697      const filters = Object.fromEntries(
698        this.filters.map(({ key, value }) => [key, value]),
699      );
700      history.pushState(null, null, `?${this.id}=` + JSON.stringify(filters));
701    },
702    paginationInit() {
703      if (!this.paginationEl) {
704        console.warn("filter pagination: el invalid");
705        return;
706      }
707
708      this.paginationEl.addEventListener("paginate", (e) => {
709        this.page = e.detail.page;
710        this.filterPagination = true;
711
712        if (e.detail?.showMore) {
713          this.showMore = e.detail.showMore;
714          this.startShowMore = e.detail.startShowMore;
715          this.endShowMore = e.detail.endShowMore;
716        } else {
717          this.showMore = false;
718        }
719        this.updateView();
720      });
721    },
722    paginationUpdate(count) {
723      this.page = 1;
724      this.showMore = false;
725
726      if (!this.paginationEl) {
727        console.warn("filter pagination: el invalid");
728        return;
729      }
730      const config = {
731        bubbles: false,
732        detail: {
733          count: count,
734        },
735      };
736      this.paginationEl.dispatchEvent(
737        new CustomEvent("paginationReset", config),
738      );
739    },
740  }));
741  Alpine.data("share", (data = {}) => ({
742    title: "",
743    text: "",
744    url: "",
745    ...data,
746
747    init() {
748      console.log("share --init", data);
749    },
750
751    trigger: {
752      ["x-on:click"]() {
753        try {
754          navigator.share({
755            title: this.title,
756            text: this.text,
757            url: this.url,
758          });
759        } catch (err) {
760          if (navigator?.clipboard) {
761            navigator.clipboard.writeText(url).then(
762              (res) =>
763                this.$dispatch("notify", {
764                  variant: "success",
765                  title: "Ссылка скопирована",
766                }),
767              (err) =>
768                this.$dispatch("notify", {
769                  variant: "danger",
770                  title: "Ошибка",
771                  message: err,
772                }),
773            );
774          } else {
775            this.$dispatch("notify", {
776              variant: "danger",
777              title: "Ошибка",
778              message: "undefined navigator.clipboard",
779            });
780          }
781        }
782      },
783    },
784  }));
785  Alpine.data("forms", () => ({
786    errors: [],
787
788    init() {},
789    signin() {
790      const data = this.valid();
791
792      if (!data) return;
793
794      const headers = {
795        "X-Requested-With": "XMLHttpRequest",
796      };
797
798      fetch("/login/", {
799        method: "POST",
800        headers: headers,
801        body: data,
802      })
803        .then((r) => r.json())
804        .then((r) => {
805          if (r?.errors) {
806            this.errors = r.errors;
807          }
808        });
809    },
810    forgotPassword() {
811      const data = this.valid();
812
813      if (!data) return;
814
815      const headers = {
816        "X-Requested-With": "XMLHttpRequest",
817      };
818
819      fetch("/forgotpassword/", {
820        method: "POST",
821        headers: headers,
822        body: data,
823      })
824        .then((r) => r.json())
825        .then((r) => {
826          if (r?.errors) {
827            this.errors = r.errors;
828          }
829
830          if (r.status === "ok") {
831            this.$dispatch("notify", {
832              variant: "success",
833              title: "Восстановление пароля",
834              message: r.data.sent_message,
835            });
836          }
837        });
838    },
839    editProfile() {
840      const data = this.valid();
841
842      if (!data) return;
843
844      const headers = {
845        "X-Requested-With": "XMLHttpRequest",
846      };
847      const json = {
848        name: `${data.get("lastname")} ${data.get("firstname")} ${data.get(
849          "middlename",
850        )}`,
851        firstname: data.get("firstname"),
852        middlename: data.get("middlename"),
853        lastname: data.get("lastname"),
854        jobtitle: data.get("jobtitle"),
855        company: data.get("company"),
856        email: [
857          {
858            value: data.get("email"),
859            ext: "",
860          },
861        ],
862        phone: [
863          {
864            value: data.get("phone"),
865            ext: "",
866          },
867        ],
868        url: [
869          {
870            value: data.get("url"),
871            ext: "",
872          },
873        ],
874        address: [
875          {
876            value: {
877              street: "",
878              city: data.get("address"),
879              region: "",
880              zip: "",
881              country: "",
882              lng: "",
883              lat: "",
884            },
885            ext: "",
886          },
887        ],
888      };
889
890      const body = new FormData();
891      body.set("data", JSON.stringify(json));
892      body.set("id", data.get("id"));
893      body.set("type", data.get("type"));
894      body.set("_csrf", data.get("_csrf"));
895
896      fetch("/webasyst/team/?module=profile&action=save", {
897        method: "POST",
898        headers: headers,
899        body: body,
900      })
901        .then((r) => r.json())
902        .then((r) => {
903          if (r.data?.errors) {
904            this.errors = r.errors;
905
906            r.errors.forEach((errList) => {
907              errList.forEach((err) => {
908                this.$dispatch("notify", {
909                  variant: "danger",
910                  title: "Ошибка!",
911                  message: err,
912                });
913              });
914            });
915          } else if (r.status === "ok") {
916            this.$dispatch("notify", {
917              variant: "success",
918              title: "Редактирование профиля",
919              message: "Профиль сохранён",
920            });
921
922            if (location) {
923              location.reload();
924            }
925          }
926        });
927    },
928    signup() {
929      const data = this.valid();
930
931      if (!data) return;
932
933      const headers = {
934        "X-Requested-With": "XMLHttpRequest",
935      };
936
937      fetch("/signup/", {
938        method: "POST",
939        headers: headers,
940        body: data,
941      })
942        .then((r) => r.json())
943        .then((r) => {
944          if (r?.errors) {
945            this.errors = r.errors;
946          }
947
948          const event = new Event("user-registered");
949          document.dispatchEvent(event);
950        });
951    },
952    feedback(toast = false) {
953      const data = this.valid();
954
955      if (!data) return;
956
957      const headers = {
958        "X-Requested-With": "XMLHttpRequest",
959      };
960
961      fetch("/leads/", {
962        method: "POST",
963        headers: headers,
964        body: data,
965      })
966        .then((r) => r.json())
967        .then((r) => {
968          if (r?.errors) {
969            this.errors = r.errors;
970
971            if (toast) {
972              r.errors.forEach((errList) => {
973                errList.forEach((err) => {
974                  this.$dispatch("notify", {
975                    variant: "danger",
976                    title: "Ошибка!",
977                    message: err,
978                  });
979                });
980              });
981            }
982            return;
983          }
984
985          const event = new Event("feedback-form-sended");
986          document.dispatchEvent(event);
987
988          this.$dispatch("notify", {
989            variant: "success",
990            title: "Успешно!",
991            message: "Заявка успешно отправлена",
992          });
993        });
994    },
995    valid() {
996      this.validation = new Validation(this.$refs.form);
997      if (!this.validation.validate()) return false;
998
999      return new FormData(this.$refs.form);
1000    },
1001  }));
1002  Alpine.data("regions", (data) => ({
1003    equipments: data.filter((item) => item.type === "equipment"),
1004    services: data.filter((item) => item.type === "service"),
1005
1006    init() {
1007      console.log("regions --init", data);
1008      initRegionsMap(data);
1009    },
1010  }));
1011  Alpine.data("toasts", () => ({
1012    toasts: [],
1013    displayDuration: 8000,
1014    soundEffect: false,
1015
1016    addToast({
1017      variant = "info",
1018      sender = null,
1019      title = null,
1020      message = null,
1021    }) {
1022      const id = Date.now();
1023      const toast = { id, variant, sender, title, message };
1024
1025      if (this.toasts.length >= 20) {
1026        this.toasts.splice(0, this.toasts.length - 19);
1027      }
1028
1029      this.toasts.push(toast);
1030    },
1031    removeToast(id) {
1032      setTimeout(() => {
1033        this.toasts = this.toasts.filter((toast) => toast.id !== id);
1034      }, 400);
1035    },
1036  }));
1037  Alpine.data("searchPages", (data = []) => ({
1038    pages: [],
1039    searchRes: [],
1040    searchQuery: "",
1041
1042    async init() {
1043      if (!Array.isArray(data)) {
1044        console.error("data is not array");
1045        return;
1046      }
1047
1048      this.pages = data.sort((a, b) =>
1049        a.title.localeCompare(b.title, undefined, {
1050          sensitivity: "base",
1051        }),
1052      );
1053
1054      // for (const page of this.pages) {
1055      //   page.img = await this.getMetaImage(page.url);
1056      // }
1057    },
1058
1059    async getMetaImage(url) {
1060      try {
1061        const res = await fetch(url);
1062        const html = await res.text();
1063
1064        const parser = new DOMParser();
1065        const doc = parser.parseFromString(html, "text/html");
1066
1067        const ogImage = doc.querySelector('meta[property="og:image"]')?.content;
1068        const twitterImage = doc.querySelector(
1069          'meta[name="twitter:image"]',
1070        )?.content;
1071        const icon = doc.querySelector('link[rel="icon"]')?.href;
1072
1073        return ogImage || twitterImage || null;
1074      } catch (err) {
1075        console.error(err);
1076        return null;
1077      }
1078    },
1079
1080    searchItems() {
1081      const query = this.searchQuery.trim().toLowerCase();
1082
1083      this.searchRes = this.pages
1084        .map((item) => {
1085          const title = (item.title || "").toString().toLowerCase();
1086          const desc = (item.desc || "").toString().toLowerCase();
1087
1088          const score =
1089            (title.includes(query) ? 2 : 0) + (desc.includes(query) ? 1 : 0);
1090
1091          return {
1092            ...item,
1093            title: this.highlight(item.title, this.searchQuery),
1094            desc: this.highlight(item.desc || "", this.searchQuery),
1095            score,
1096          };
1097        })
1098        .filter((item) => item.score > 0)
1099        .sort((a, b) => b.score - a.score);
1100    },
1101
1102    highlight(text, query) {
1103      const regex = new RegExp(`(${query})`, "gi");
1104      return text.replace(regex, "<mark>$1</mark>");
1105    },
1106  }));
1107  Alpine.data("video", (src = "") => ({
1108    paused: true,
1109    src: src.trim(),
1110    isLoading: false,
1111    videoAppended: false,
1112    videoClass: "player w-full h-full object-contain relative z-10",
1113    videoBgClass: "player w-full h-full object-cover relative z-0",
1114    observer: null,
1115    isStarted: false,
1116
1117    init() {
1118      if (!this.src) return;
1119
1120      this.watchVideoScope();
1121    },
1122    watchVideoScope() {
1123      if (!("IntersectionObserver" in window)) {
1124        console.warn(
1125          "IntersectionObserver not supported, loading video immediately.",
1126        );
1127        this.loadAndAppendVideo();
1128        return;
1129      }
1130
1131      this.observer = new IntersectionObserver(
1132        (entries, observer) => {
1133          entries.forEach((entry) => {
1134            if (entry.isIntersecting) {
1135              this.loadAndAppendVideo();
1136              observer.unobserve(this.$el);
1137            }
1138          });
1139        },
1140        {
1141          root: null,
1142          rootMargin: "0px",
1143          threshold: 0.75,
1144        },
1145      );
1146
1147      this.observer.observe(this.$el);
1148    },
1149    async loadAndAppendVideo() {
1150      if (this.videoAppended) return;
1151
1152      this.isLoading = true;
1153
1154      const isUrlValid = await this.checkVideoUrl();
1155      if (!isUrlValid) {
1156        this.handleLoadingError();
1157        return;
1158      }
1159
1160      const video = document.createElement("video");
1161      video.controls = false;
1162      video.playsInline = "";
1163      video.preload = "metadata";
1164      video.setAttribute("class", this.videoClass);
1165      video.setAttribute("x-show", "isStarted");
1166      video.setAttribute("x-cloak", "");
1167      video.src = this.src;
1168
1169      const videoBg = video.cloneNode(true);
1170      videoBg.setAttribute("class", this.videoBgClass);
1171      videoBg.setAttribute("x-show", "videoAppended");
1172
1173      video.addEventListener("loadedmetadata", () => {
1174        video.currentTime = 0.1;
1175        this.isLoading = false;
1176      });
1177
1178      video.addEventListener("error", (e) => {
1179        this.handleLoadingError();
1180      });
1181      videoBg.addEventListener("error", (e) => {
1182        console.error("Error loading video:", videoBg.error);
1183      });
1184
1185      if (this.$refs.bg) {
1186        this.$refs.bg.prepend(videoBg);
1187      }
1188      /*video.addEventListener("seeked", () => {
1189        this.posterImage(video);
1190      });*/
1191
1192      this.$el.prepend(video);
1193      this.videoAppended = true;
1194
1195      this.controlsVideo(video, videoBg);
1196    },
1197    posterImage(video = null) {
1198      if (!video) return;
1199
1200      const canvas = document.createElement("canvas");
1201      const ctx = canvas.getContext("2d");
1202
1203      canvas.width = video.videoWidth;
1204      canvas.height = video.videoHeight;
1205
1206      ctx.drawImage(video, 0, 0);
1207
1208      this.$el.style.backgroundImage = `url(${canvas.toDataURL()})`;
1209    },
1210    controlsVideo(video = null, videoBg = null) {
1211      if (!video) return;
1212
1213      this.$el.addEventListener(
1214        "click",
1215        () => {
1216          if (!this.videoAppended || this.isLoading || !video) return;
1217
1218          if (video.paused) {
1219            video.play().then(() => {
1220              if (this.isStarted) return;
1221
1222              this.isStarted = true;
1223            });
1224
1225            videoBg && videoBg.play();
1226          } else {
1227            video.pause();
1228
1229            videoBg && videoBg.pause();
1230          }
1231
1232          this.paused = video.paused;
1233        },
1234        true,
1235      );
1236    },
1237    handleLoadingError() {
1238      console.error("Error loading video:", this.src);
1239      this.isLoading = false;
1240      this.videoAppended = false;
1241    },
1242    async checkVideoUrl() {
1243      return new Promise((res) => {
1244        const video = document.createElement("video");
1245        video.preload = "metadata";
1246
1247        const timeout = setTimeout(() => {
1248          video.remove();
1249          res(false);
1250        }, 5000);
1251
1252        video.addEventListener("loadedmetadata", () => {
1253          clearTimeout(timeout);
1254          video.remove();
1255          res(true);
1256        });
1257
1258        video.addEventListener("error", () => {
1259          clearTimeout(timeout);
1260          video.remove();
1261          res(false);
1262        });
1263
1264        video.src = this.src;
1265      });
1266    },
1267  }));
1268});
1269
1270class Validation {
1271  constructor(form = undefined) {
1272    // console.log("Validation init", form);
1273
1274    this.form = form;
1275    this.novalid = [];
1276
1277    if (!this.form) this.inputs = [];
1278    else
1279      this.inputs = Array.from(
1280        this.form.querySelectorAll("input[required], textarea[required]"),
1281      );
1282  }
1283
1284  validate() {
1285    this.novalid = [];
1286    this.inputs.forEach((input) => {
1287      /*console.log(
1288        "validation",
1289        input,
1290        input.type,
1291        input.value,
1292        input.checked,
1293        input.dataset.requiredMessage,
1294        input.closest(".form-group"),
1295      );*/
1296      if (
1297        (input.type === "checkbox" && input.checked) ||
1298        (input.type !== "checkbox" && input.value.length > 0)
1299      )
1300        return;
1301
1302      this.novalid.push(input);
1303
1304      input.dataset.errorValid = "true";
1305
1306      function removeErrorMessage() {
1307        input.dataset.errorValid = "false";
1308        input.removeEventListener("input", removeErrorMessage);
1309      }
1310
1311      input.addEventListener("input", removeErrorMessage);
1312    });
1313
1314    return this.novalid.length === 0;
1315  }
1316
1317  createElMessage() {
1318    const p = document.createElement("p");
1319    p.classList.add("input-message-error");
1320
1321    return p;
1322  }
1323}

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.