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.