PageSourceSearch

https://pics.loveplanet.ru/css/3/imgstc/lp14/controls.js

js loveplanet.ru collected 2026-09-25 09:12:47 UTC 58,705 bytes, 2,105 lines download raw bytes

1/*
2При использовании тега <select> обзательно прописывать класс
3'select-control' и атрибут 'data-type', имеющий следующие значения:
4simple - для обычного <select>, например, выбор цели знакомства или даты рождения;
5simple-search - для обычного <select> с возможностью выбора или поиска по options;
6geo-search - <select> с возможностью выбора или поиска населённого пункта
7*/
8
9const Controll = (() => {
10	'use strict';
11
12	const MINREQ = 2;
13	const MINREQTAG = 3;
14
15	class SelectSimple {
16		constructor(s) {
17			this.s = s;
18			this.class = s.className;
19			this.name = s.getAttribute('name');
20			this.type = s.dataset.type;
21			this.options = s.options;
22			this.parent = s.parentNode;
23			this.searchArray = [];
24
25			s.hidden = true;
26
27			this.createElementsSimple('createInitalize');
28			this.setHandlers();
29		}
30
31		createElementsSimple(fname) {
32			let panel, content;
33
34			const fn = {
35				createInitalize: () => {
36					this.container = document.createElement('div');
37					this.container.className = `${this.class} select-container`;
38					this.container.setAttribute('tabindex', Math.floor(Math.random() * 1000));
39					if (this.s.hasAttribute('data-targid')) {
40						this.container.id = this.s.id;
41						this.container.dataset.targid = this.s.dataset.targid;
42					}
43
44					this.select = fn.createSelect();
45					this.dropPanel = fn.createDropPanel('drop-panel');
46
47					this.container.append(this.select);
48					this.container.append(this.dropPanel);
49					this.s.after(this.container);
50
51					this.scrollbox = new ScrollBox(this.dropPanel);
52				},
53
54				createSelect: () => {
55					const select = document.createElement('div');
56					select.className = 'select-input';
57
58					this.txtnode = document.createElement('div');
59					this.txtnode.className = 'select-text';
60
61					this.arrow = document.createElement('i');
62					this.arrow.className = 'icon-stack down-gray';
63
64					select.append(this.txtnode);
65					select.append(this.arrow);
66
67					this.setSelectedItemData();
68
69					return select;
70				},
71
72				createDropPanel: () => {
73					const [panel, content] = fn.createScrollElements();
74					this.contentBox = content;
75
76					let list = document.createElement('ul');
77					list.className = 'reset options-list';
78					this.ul = this.otputListItemsDrop(list);
79					this.items = this.ul.children;
80
81					content.append(this.ul);
82					panel.append(content);
83
84					return panel;
85				},
86
87				createScrollElements: () => {
88					panel = document.createElement('div');
89					panel.className = 'scroll-container drop-panel';
90
91					content = document.createElement('div');
92					content.className = 'scroll-content';
93
94					return [panel, content];
95				},
96			};
97
98			fn[fname]();
99			return [panel, content];
100		}
101
102		reInitalize() {
103			// заменить на [...this.ul.children] или
104			// очистку this.ul через while
105			// for (let ch of [].slice.call(this.ul.children)) ch.remove();
106			this.setSelectedItemData();
107			while (this.ul.firstChild) {
108				this.ul.removeChild(this.ul.firstChild);
109			}
110			this.ul = this.otputListItemsDrop(this.ul);
111			this.items = this.ul.children;
112		}
113
114		otputListItemsDrop(list) {
115			let i = -1;
116			const frag = document.createDocumentFragment();
117			this.searchArray['first'] = [];
118
119			for (let item of this.options) {
120				// используется если первый элемент списка пустой, для <select>
121				// устанавливаем 'data-firstitem' для удаления элемента из списка и прописывания нулевых значений
122				i++;
123				if (this.s.dataset.firstitem === 'false' && i == 0) {
124					continue;
125				} else if (this.s.dataset.firstitem === 'true' && i == 0) {
126					this.txtnode.innerHTML = '';
127					this.s.value = 0;
128					continue;
129				}
130
131				let li = document.createElement('li');
132				// выбор фона в геочате
133				let ophtml = item.dataset.ophtml || void 0;
134
135				li.dataset.value = item.value;
136				li.innerHTML = ophtml ? ophtml : item.innerHTML;
137
138				if (item.hasAttribute('selected')) {
139					this.selected = li;
140					li.classList.add('selected');
141					li.classList.add('current');
142				}
143
144				this.searchArray['first'].push({
145					value: item.value,
146					name: item.textContent,
147				});
148
149				frag.append(li);
150			}
151
152			list.append(frag);
153
154			return list;
155		}
156
157		setSelectedItemData() {
158			if ((this.s.selectedIndex || 0) <= 0) this.s.selectedIndex = 0;
159			const curr = this.options[this.s.selectedIndex];
160
161			for (let item of this.options) {
162				if (item == curr) item.setAttribute('selected', 'selected');
163				else item.removeAttribute('selected');
164			}
165
166			if (this.type === 'simple') {
167				if (curr) this.txtnode.innerHTML = curr.innerHTML;
168			} else {
169				if (!this.inputElement) return;
170
171				this.inputElement.value = curr.innerHTML;
172				this.inputElement.placeholder = curr.textContent;
173			}
174		}
175
176		setHandlers() {
177			document.addEventListener('keydown', e => this.handlers(e, 'onKeyDown'));
178			this.select.addEventListener('click', e => this.handlers(e, 'toggleDropPanel'));
179			this.s.addEventListener('change', e => this.reInitalize());
180			this.container.addEventListener('keydown', e => this.keyControl(e, 'onKeyDown'));
181			this.ul.addEventListener('click', e => this.handlers(e, 'selectItem'));
182
183			// ф-я назначена переменной, чтобы была возможность удалить
184			// обработчик события
185			this.handler = e => this.handlers(e, 'onMouseOverItem');
186			this.ul.addEventListener('mouseover', this.handler);
187
188			if (['simple-search', 'geo-search'].includes(this.type)) return;
189
190			this.container.addEventListener('focusout', () => {
191				setTimeout(() => this.handlers(null, 'hideDropPanel'), 250);
192			}, true);
193		}
194
195		handlers(e, fname) {
196			const fn = {
197				onKeyDown: () => {
198					if (e.code !== 'Escape') return;
199					this.handlers(null, 'hideDropPanel');
200				},
201
202				toggleDropPanel: () => {
203					this.arrow.classList.toggle('rotate');
204					this.container.classList.toggle('active');
205
206					if (this.container.matches('.active')) {
207						let sel = null;
208						for (let item of this.items) {
209							if (item.matches('.selected')) {
210								item.classList.add('current');
211								sel = item;
212							} else {
213								item.classList.remove('current');
214							}
215						}
216
217						this.scrollbox.initialize();
218						if (sel) sel.scrollIntoView({ block: 'nearest' });
219					}
220				},
221
222				hideDropPanel: () => {
223					this.arrow.classList.remove('rotate');
224					this.container.classList.remove('active');
225				},
226
227				selectItem: () => {
228					const el = this.setSelected(e);
229					if (!el) return;
230
231					this.txtnode.innerHTML = el.innerHTML;
232
233					fn.hideDropPanel();
234				},
235
236				onMouseOverItem: () => {
237					let el = e.target;
238					while (!el.matches('li') && (el = el.parentElement));
239
240					for (let item of this.items) {
241						item.classList.remove('current');
242					}
243
244					el.classList.add('current');
245				},
246			};
247
248			fn[fname]();
249		}
250
251		keyControl(e, fname) {
252			const fn = {
253				onKeyDown: () => {
254					if (e.code && !['ArrowDown', 'ArrowUp', 'Enter', 'NumpadEnter'].includes(e.code)) {
255						this.container.querySelector('.scroll-content').scrollTop = 0;
256						return;
257					}
258					if (['Enter', 'NumpadEnter'].includes(e.code)) {
259						fn.selectItem(e);
260					} else if (['ArrowDown', 'ArrowUp'].includes(e.code)) {
261						fn.movementItem(e);
262					}
263				},
264
265				getIndexForMovement: () => {
266					return [...this.items].findIndex(item => item.matches('.current'));
267				},
268
269				movementItem: (e) => {
270					e.preventDefault();
271
272					this.ul.removeEventListener('mouseover', this.handler);
273
274					let i = fn.getIndexForMovement();
275
276					let next = (e.code === 'ArrowDown') ? i + 1 : i - 1;
277					// if (next < 0 || next >= this.items.length) return;
278					if (next < 0) next = this.items.length - 1;
279					if (next >= this.items.length) next = 0;
280
281					this.items[next].classList.add('current');
282					if (i >= 0) {
283						this.items[i].classList.remove('current');
284					}
285
286					this.items[next].scrollIntoView({ block: 'nearest' });
287
288					setTimeout(() => this.ul.addEventListener('mouseover', this.handler), 5);
289				},
290
291				selectItem: (e) => {
292					let i = fn.getIndexForMovement();
293					const el = this.items[i];
294					el.dispatchEvent(new Event('click', { bubbles: true }));
295				},
296			};
297
298			fn[fname]();
299		}
300
301		setSelected(e) {
302			let el = e.target;
303			while (!el.matches('li') && (el = el.parentElement));
304
305			if (!el.matches('li') || el.dataset.search === 'empty' || el.hasAttribute('disabled')) return false;
306
307			for (let item of this.items) {
308				item.classList.remove('selected');
309			}
310			el.classList.add('selected');
311
312			for (let item of this.options) {
313				item.removeAttribute('selected');
314			}
315
316			this.selected = el;
317			this.s.value = el.dataset.value;
318
319			const i = this.s.selectedIndex;
320			this.options[i].setAttribute('selected', 'selected');
321
322			this.s.dispatchEvent(new Event('change'));
323			return el;
324		}
325	}
326
327	class SelectSimpleSearch extends SelectSimple {
328		constructor(s) {
329			super(s);
330			this.type = s.dataset.type;
331
332			this.createElementsSimpleSearch('createInitalize');
333			this.setHandlersSimpleSearch();
334		}
335
336		createElementsSimpleSearch(fname) {
337			const fn = {
338				createInitalize: () => {
339					this.dropPanel.id = 'dropsearch';
340					fn.createSelectInput();
341				},
342
343				createSelectInput: () => {
344					this.inputElement = document.createElement('input');
345					this.setSelectedItemData();
346					this.select.prepend(this.inputElement);
347
348					this.txtnode.remove();
349				},
350			};
351
352			fn[fname]();
353		}
354
355		outputSearchResultEmpty() {
356			if (this.ul.firstChild.dataset.search === 'empty') return;
357
358			let li = document.createElement('li');
359			li.dataset.search = 'empty';
360			li.innerText = lng('Ничего не найдено');
361
362			this.clearing('removeChildren');
363			this.ul.prepend(li);
364
365			this.scrollbox.initialize();
366		}
367
368		setHandlersSimpleSearch() {
369			this.select.addEventListener('input', e => this.getSearchResult(e));
370			this.inputElement.addEventListener('blur', () => {
371				if (this.scrollbox.pressed) return;
372				setTimeout(() => this.handlers(null, 'hideDropPanel'), 300);
373			});
374		}
375
376		handlers(e, fname) {
377			const fn = {
378				onKeyDown: () => {
379					super.handlers(e, fname);
380				},
381
382				toggleDropPanel: () => {
383					if (e.target.matches('.click-zone')) return;
384
385					super.handlers(null, 'toggleDropPanel');
386
387					if (this.container.classList.contains('active')) {
388						if (this.istags) this.clearing('removeChildrenOfTags');
389
390						this.inputElement.value = '';
391						this.inputElement.focus();
392					} else {
393						this.inputElement.value = this.inputElement.placeholder;
394						this.inputElement.blur();
395					}
396				},
397
398				hideDropPanel: () => {
399					this.inputElement.value = this.inputElement.placeholder;
400					super.handlers(null, 'hideDropPanel');
401					this.inputElement.blur();
402				},
403
404				selectItem: () => {
405					const el = this.setSelected(e);
406					if (!el) return;
407
408					this.inputElement.placeholder = el.textContent;
409
410					fn.hideDropPanel();
411				},
412
413				onMouseOverItem: () => {
414					super.handlers(e, 'onMouseOverItem');
415				},
416			};
417
418			fn[fname]();
419		}
420
421		getSearchResult(e) {
422			this.str = e.target.value.toLowerCase().trim();
423
424			let searchResult = this.searchArray['first'].filter(item => item.name.toLowerCase().includes(this.str));
425
426			if (searchResult.length === 0) {
427				this.outputSearchResultEmpty();
428				return;
429			}
430
431			const frag = document.createDocumentFragment();
432
433			searchResult.forEach(item => {
434				if (!item.value) return;
435
436				let li = document.createElement('li');
437				li.dataset.value = item.value;
438				li.innerHTML = `<div>${this.setHighlights(item.name)}</div>`;
439
440				frag.append(li);
441			});
442
443			this.clearing('removeChildren');
444			this.ul.prepend(frag);
445			this.istags = true;
446
447			this.scrollbox.initialize();
448		}
449
450		clearing(fname) {
451			const fn = {
452				removeChildren: () => {
453					while (this.ul.firstChild) {
454						this.ul.removeChild(this.ul.firstChild);
455					}
456				},
457
458				removeChildrenOfTags: () => {
459					fn.removeChildren();
460
461					const frag = document.createDocumentFragment();
462
463					for (let item of this.options) {
464						let li = document.createElement('li');
465						li.dataset.value = item.value;
466						li.innerHTML = item.innerHTML;
467
468						if (item.value === this.selected.dataset.value) {
469							li.classList.add('active');
470						} else {
471							li.classList.remove('active');
472						}
473
474						frag.append(li);
475					}
476
477					this.ul.prepend(frag);
478
479					this.scrollbox.initialize();
480
481					this.istags = false;
482				},
483			};
484
485			fn[fname]();
486		}
487
488		setHighlights(text) {
489			return text.replace(new RegExp(`${this.str}`, 'i'), '<b>$&</b>');
490		}
491	}
492
493	class SelectGeoSearch extends SelectSimpleSearch {
494		constructor(s) {
495			super(s);
496			this.type = s.dataset.type;
497			this.geo = s.dataset.geo;
498
499			this.createCleaner();
500			this.setHandlersGeoSearch();
501		}
502
503		createCleaner() {
504			this.cleaner = document.createElement('div');
505			this.cleaner.className = 'flex click-zone';
506			this.arrow.before(this.cleaner);
507
508			this.dropPanel.id = 'dropgeo';
509		}
510
511		setHandlersGeoSearch() {
512			this.cleaner.addEventListener('click', () => this.clearing('clearingSelect'));
513			this.select.addEventListener('input', e => this.searchCity(e, 'getSearchResult'));
514		}
515
516		searchCity(e, fname, data) {
517			const fn = {
518				getSearchResult: () => {
519					this.str = e.target.value.toLowerCase();
520					const length = this.str.length;
521
522					if (length < MINREQ) {
523						fn.outpuSearchResultSingleLetter();
524					} else if (length === MINREQ) {
525						if (this.searchArray[this.str]) {
526							this.currentSearchArray = this.searchArray[this.str];
527							fn.outputSearchResultFromArray(this.currentSearchArray);
528							fn.createOptionSearchResult(this.currentSearchArray);
529						} else {
530							const url = this.getRequestURL(this.str, this.geo);
531							this.getJSON(url);
532						}
533					} else {
534						if (!this.currentSearchArray || this.currentSearchArray.length === 0) {
535							this.outputSearchResultEmpty();
536							return;
537						}
538						const arr = this.currentSearchArray.filter(item => item.name.toLowerCase().includes(this.str));
539						fn.outputSearchResultFromArray(arr);
540					}
541				},
542
543				// вывод результата поиска по одной букве
544				outpuSearchResultSingleLetter: () => {
545					let searchResult = this.searchArray['first'].filter(item => item.name[0].toLowerCase() === this.str);
546
547					if (searchResult.length === 0) {
548						this.outputSearchResultEmpty();
549						return;
550					}
551
552					const frag = document.createDocumentFragment();
553
554					searchResult.forEach(item => {
555						if (!item.value) return;
556
557						let li = document.createElement('li');
558						li.dataset.value = item.value;
559						li.innerHTML = `<div>${this.setHighlights(item.name)}</div>`;
560
561						frag.append(li);
562					});
563
564					this.clearing('removeChildren');
565					this.ul.prepend(frag);
566					this.istags = true;
567
568					this.scrollbox.initialize();
569				},
570
571				// вывод результата поиска из сохранённого массива
572				outputSearchResultFromArray: arr => {
573					if (arr.length === 0) {
574						this.outputSearchResultEmpty();
575						return;
576					}
577
578					const frag = document.createDocumentFragment();
579					const skip = [3159, 9908];
580
581					arr.forEach(item => {
582						let li = document.createElement('li');
583
584						let nameCountry = item.name_country || '';
585						let name = '';
586
587						if (!item.code_country) {
588							item.code_country = item.code;
589							item.code_region = item.code = 0;
590						} else
591							if (!item.code_region) {
592								item.code_region = item.code;
593								item.code = 0;
594							}
595
596						li.dataset.value = `${item.code_country},${item.code_region},${item.code}`;
597						if (nameCountry.length) {
598							li.dataset.country = nameCountry;
599							if (!skip.includes(item.code_country)) name = `, ${nameCountry}`;
600						}
601						li.innerHTML = `<div>${this.setHighlights(item.name)}${name}</div>`;
602
603						frag.append(li);
604					});
605
606					this.clearing('removeChildren');
607					this.ul.prepend(frag);
608					this.istags = true;
609
610					this.scrollbox.initialize();
611				},
612
613				// вывод результата поиска через fetch
614				outputSearchResultFromData: () => {
615					fn.outputSearchResultFromArray(data);
616					fn.createOptionSearchResult(data);
617
618					this.searchArray[this.str] = data.slice();
619					this.currentSearchArray = this.searchArray[this.str];
620					data = [];
621				},
622
623				createOptionSearchResult: arr => {
624					if (arr.length === 0) return;
625
626					while (this.s.firstChild) {
627						this.s.removeChild(this.s.firstChild);
628					}
629
630					const frag = document.createDocumentFragment();
631
632					arr.forEach(item => {
633						let option = document.createElement('option');
634
635						let codeCountry = item.code_country || 0;
636						let codeRegion = item.code_region || 0;
637						let nameCountry = item.name_country || '';
638
639						option.value = `${codeCountry},${codeRegion},${item.code}`;
640						option.innerHTML = `${item.name}, ${nameCountry}`;
641
642						frag.append(option);
643					});
644
645					this.s.prepend(frag);
646				},
647			};
648
649			fn[fname]();
650		}
651
652		clearing(fname, panelIndex) {
653			const fn = {
654				clearingSelect: () => {
655					this.inputElement.value = lng('Не имеет значения');
656					this.inputElement.placeholder = lng('Не имеет значения');
657					this.s.value = '0,0,0';
658
659					// добавляем данный пункт в список результата поиска
660					const val = this.ul.firstChild.dataset.value;
661					if (val !== this.s.value) {
662						const li = document.createElement('li');
663
664						li.dataset.value = this.s.value;
665						li.innerHTML = this.inputElement.placeholder;
666
667						this.ul.prepend(li);
668					}
669
670					const items = this.ul.childNodes;
671					for (let item of items) {
672						item.classList.remove('selected');
673					}
674					this.ul.firstChild.classList.add('selected');
675
676					this.scrollbox.initialize();
677
678					super.handlers(null, 'hideDropPanel');
679				},
680
681				removeChildren: () => {
682					super.clearing('removeChildren', panelIndex);
683				},
684
685				removeChildrenOfTags: () => {
686					const items = this.ul.childNodes;
687
688					for (let item of items) {
689						item.innerHTML = item.textContent;
690					}
691					this.istags = false;
692				},
693			};
694
695			fn[fname]();
696		}
697
698		getLocation() {
699			return this.s.value.split(',');
700		}
701
702		getRequestURL(str, geo) {
703			return `/api/a-geo/type-${geo || 9}/name-${str}`;
704		}
705
706		async getJSON(url) {
707			try {
708				const response = await fetch(url);
709				const data = await response.json();
710				this.searchCity(null, 'outputSearchResultFromData', data.geo);
711			} catch (err) {
712				console.log(err);
713			}
714		}
715	}
716
717	// select factory
718	let selectsArr = [];
719
720	function selectsArrSearch(el) {
721		for (let sobj of selectsArr) {
722			if (sobj.s === el) return sobj;
723		}
724		return null;
725	}
726
727	function initSelect(context = document) {
728		const sel = context.querySelectorAll('select');
729
730		for (let s of sel) {
731			if (selectsArrSearch(s)) continue;
732
733			if (s.dataset.type === 'simple-search') {
734				s = new SelectSimpleSearch(s);
735			} else if (s.dataset.type === 'geo-search') {
736				s = new SelectGeoSearch(s);
737			} else {
738				s = new SelectSimple(s);
739			}
740			selectsArr.push(s);
741		}
742	}
743
744	function reinitSelect() {
745		for (let sobj of selectsArr) {
746			sobj.reInitalize();
747		}
748	}
749
750	// custom Scroll
751	class ScrollBox {
752		constructor(container) {
753			// return;
754			this.container = container;
755			this.contentBox = container.querySelector('.scroll-content');
756
757			this.pressed = false;
758			this.maxheight = 200;
759			this.scrollmin = 20;
760			this.barier = 4;
761
762			this.initialize();
763			this.registerEventsHandler();
764		}
765
766		initialize() {
767			if (!this.scrollbar) {
768				this.scrollbar = document.createElement('div');
769				this.scroller = document.createElement('div');
770
771				this.scrollbar.className = 'scrollbar';
772				this.scroller.className = 'scroller';
773
774				this.scrollbar.append(this.scroller);
775				this.container.append(this.scrollbar);
776			}
777
778			let [ch, sh] = this.getFrameSize();
779			// возможная максимальная прокрутка контента
780			this.max = sh - ch;
781			// соотношение между высотами вьюпорта и контента
782			this.ratio = ch / sh;
783
784			this.clientHeight = ch;
785			this.scrollHeight = sh;
786
787			if (this.max > 0) {
788				this.scrollerHeight = parseInt(this.ratio * ch);
789				this.scrollerHeight = (this.scrollerHeight < this.scrollmin) ? this.scrollmin : this.scrollerHeight;
790				this.scrollbar.style.display = '';
791
792				this.ratio = (ch - this.scrollerHeight - (this.barier * 2)) / this.max;
793			} else {
794				this.scrollbar.style.display = 'none';
795			}
796
797			this.container.style.height = `${ch}px`;
798			this.scroller.style.height = `${this.scrollerHeight}px`;
799			this.scroller.style.top = `${this.barier}px`;
800		}
801
802		getFrameSize() {
803			let sh = 0, ch = 0;
804			let li = this.contentBox.querySelectorAll('li');
805			for (let item of li) {
806				sh += item.getBoundingClientRect().height;
807			}
808
809			ch = (sh < this.maxheight) ? sh : this.maxheight;
810			return [ch, sh];
811		}
812
813		registerEventsHandler() {
814			this.contentBox.addEventListener('scroll', () => {
815				this.scroller.style.top = `${this.contentBox.scrollTop * this.ratio + this.barier}px`;
816			});
817
818			this.scroller.addEventListener('mousedown', e => {
819				this.start = e.screenY;
820				this.pressed = true;
821			});
822
823			document.addEventListener('mousemove', e => this.drop(e));
824			document.addEventListener('mouseup', () => this.pressed = false);
825		}
826
827		drop(e) {
828			e.preventDefault();
829			if (this.pressed === false) return;
830
831			this.contentBox.scrollTop -= (this.start - e.screenY) / this.ratio;
832			this.start = e.screenY;
833		}
834	}
835
836	class Slider {
837		constructor(setup) {
838			this.slider = setup.slider;
839			this.width = setup.width;
840			this.title = setup.title;
841			this.range = setup.range;
842			this.index = setup.index;
843			// по длине массива определяем кол-во ползунков
844			this.start = setup.start.slice(0, 2);
845
846			this.createSlider();
847		}
848
849		createSlider() {
850			const header = document.createElement('div');
851			header.className = 'range-header';
852
853			const title = document.createElement('div');
854			title.className = 'range-title';
855			title.innerHTML = this.title;
856
857			const output = document.createElement('div');
858			output.className = 'flex range-output';
859
860			const control = document.createElement('div');
861			control.className = 'range-control';
862
863			const scrollbar = document.createElement('div');
864			scrollbar.className = 'range-scrollbar';
865
866			const scale = document.createElement('div');
867			scale.className = 'range-scale';
868
869			header.append(title);
870			header.append(output);
871			scrollbar.append(scale);
872			control.append(scrollbar);
873
874			this.slider.append(header);
875			this.slider.append(control);
876
877			const elements = { output, scrollbar, scale };
878
879			for (let i = 0, l = this.start.length; i < l; i++) {
880				const inst = new Scroller(this, elements, this.start[i], i);
881				inst.creatScroller();
882			}
883		}
884	}
885
886	class Scroller {
887		constructor({ width, range, start, index }, { output, scrollbar, scale }, value, i) {
888			this.width = width;
889			this.min = range.min;
890			this.max = range.max;
891			this.start = start;
892			this.index = index[i];
893			this.output = output;
894			this.scrollbar = scrollbar;
895			this.scale = scale;
896			this.value = value;
897			this.i = i;
898			this.dragObject = {};
899			this.pressed = false;
900			this.events = false;
901		}
902
903		xpos(e) {
904			// touch event
905			if (e.targetTouches && (e.targetTouches.length >= 1)) {
906				return e.targetTouches[0].clientX;
907			}
908			// mouse event
909			return e.clientX;
910		}
911		computedX(x, min, max) {
912			return (x <= min) ? min : (x >= max) ? max : x;
913		}
914
915		creatScroller() {
916			this.input = document.createElement('input');
917			this.input.name = this.index;
918			this.input.type = 'hidden';
919			this.scrollbar.prepend(this.input);
920
921			this.scroller = document.createElement('div');
922			this.scroller.className = 'range-scroller';
923			this.scrollbar.append(this.scroller);
924
925			const sh = this.initialize();
926			this.setStartPositon(sh);
927		}
928
929		initialize() {
930			let scrollbarWidth, halfScrollerWidth;
931
932			// если range скрыт
933			if (!this.scrollbar.offsetWidth) {
934				let [a, b] = this.getDataStyle();
935				scrollbarWidth = a;
936				halfScrollerWidth = b;
937			}
938
939			this.baseWidth = this.scrollbar.offsetWidth || scrollbarWidth;
940			this.offset = this.scroller.offsetWidth / 2 || halfScrollerWidth;
941			this.ratio = this.baseWidth / (this.max - this.min);
942
943			this.registerEventsHandler();
944
945			// постоянный сдвиг ползунка
946			let sh = (this.start.length === 1) ? this.offset : (this.i === 0) ? this.offset * 2 : 0;
947			return sh;
948		}
949
950		setStartPositon(sh) {
951			const x = (this.value - this.min) * this.ratio - sh;
952			this.scroller.style.left = `${x}px`;
953
954			this.createStartPositon();
955			this.showScale();
956		}
957
958		setResizePosition(sh) {
959			const v = (this.i === 0) ? this.start[0] : this.start[1];
960			const x = (v - this.min) * this.ratio - sh;
961
962			this.scroller.style.left = `${x}px`;
963
964			this.showScale();
965		}
966
967		getDataStyle() {
968			const root = document.querySelector('.range');
969			const rootStyles = getComputedStyle(root);
970			const wslide = parseInt(rootStyles.getPropertyValue('--scroller-size'));
971
972			let scrollbarWidth = (this.start.length === 1) ? this.width - wslide : this.width - wslide * 2;
973			let halfScrollerWidth = wslide / 2;
974
975			return [scrollbarWidth, halfScrollerWidth];
976		}
977
978		createStartPositon() {
979			this.span = document.createElement('span');
980			this.span.id = this.index;
981			this.span.innerHTML = this.input.value = this.value;
982			this.output.append(this.span);
983		}
984
985		showSlideValue() {
986			this.span.innerHTML = this.input.value = this.start[this.i];
987		}
988
989		showScale() {
990			const [x0, x1] = this.start;
991			let x, w;
992
993			if (this.start.length === 1) {
994				x = -this.offset;
995				w = (x0 - this.min) * this.ratio + 3;
996			} else {
997				x = (x0 - this.min) * this.ratio - 1;
998				w = (x1 - x0) * this.ratio + 3;
999			}
1000
1001			const st = `width: ${w}px; left: ${x}px;`;
1002			this.scale.style.cssText = st;
1003		}
1004
1005		registerEventsHandler() {
1006			if (this.events) return;
1007
1008			window.addEventListener('resize', () => {
1009				setTimeout(() => {
1010					let sh = this.initialize();
1011					this.setResizePosition(sh);
1012				}, 26);
1013			}, false);
1014
1015			// mouse events
1016			this.scroller.addEventListener('mousedown', e => this.tap(e), false);
1017			document.addEventListener('mousemove', e => this.drag(e), false);
1018			document.addEventListener('mouseup', e => this.release(e), false);
1019
1020			// touch events
1021			this.scroller.addEventListener('touchstart', e => this.tap(e), false);
1022			document.addEventListener('touchmove', e => this.drag(e), false);
1023			document.addEventListener('touchend', e => this.release(e), false);
1024
1025			this.events = true;
1026		}
1027
1028		tap(e) {
1029			e.preventDefault();
1030
1031			if (e.button && e.button !== 0) return;
1032
1033			this.pressed = true;
1034			this.dragObject = { el: e.target, start: this.xpos(e) };
1035		}
1036
1037		drag(e) {
1038			if (!this.pressed) return;
1039
1040			let shiftX = this.dragObject.start - this.xpos(e);
1041			let x = this.dragObject.el.offsetLeft - shiftX;
1042			let min, max;
1043
1044			if (this.start.length === 1) {
1045				min = -this.offset;
1046				max = this.baseWidth - this.offset;
1047
1048				x = this.computedX(x, min, max);
1049
1050				this.start[0] = Math.round((x + this.offset) / this.ratio) + this.min;
1051			} else {
1052				if (this.i === 0) {
1053					min = -this.offset * 2;
1054					max = this.dragObject.el.nextElementSibling.offsetLeft - this.offset * 2;
1055
1056					x = this.computedX(x, min, max);
1057
1058					this.start[0] = Math.round((x + this.offset * 2) / this.ratio) + this.min;
1059				} else {
1060					min = this.dragObject.el.previousElementSibling.offsetLeft + this.offset * 2;
1061					max = this.baseWidth;
1062
1063					x = this.computedX(x, min, max);
1064
1065					this.start[1] = Math.round(x / this.ratio) + this.min;
1066				}
1067			}
1068
1069			this.dragObject.el.style.left = `${x}px`;
1070			this.showSlideValue();
1071			this.showScale();
1072
1073			this.dragObject.start = this.xpos(e);
1074		}
1075
1076		release() {
1077			this.pressed = false;
1078			this.dragObject = {};
1079		}
1080	}
1081
1082	class SearchTags {
1083		constructor(tagscontainer, container, tagsString) {
1084			this.tagsContainer = tagscontainer;
1085			this.container = container;
1086			this.tagsString = tagsString;
1087			this.input = document.querySelector('.tag-input');
1088			this.searchArray = [];
1089			this.currArray = [];
1090			this.tags = []; // теги выбранные юзером
1091
1092			this.createElements('createInitalize');
1093			this.setHandlers();
1094		}
1095
1096		createElements(fname) {
1097			let panel, content;
1098
1099			const fn = {
1100				createInitalize: () => {
1101					this.dropPanel = fn.createDropPanel('drop-panel');
1102					this.container.append(this.dropPanel);
1103
1104					this.scrollbox = new ScrollBox(this.dropPanel);
1105				},
1106
1107				createDropPanel: () => {
1108					const [panel, content] = fn.createScrollElements();
1109					this.contentBox = content;
1110
1111					this.ul = document.createElement('ul');
1112					this.ul.className = 'reset options-list';
1113
1114					content.append(this.ul);
1115					panel.append(content);
1116
1117					return panel;
1118				},
1119
1120				createScrollElements: () => {
1121					panel = document.createElement('div');
1122					panel.className = 'scroll-container drop-panel';
1123
1124					content = document.createElement('div');
1125					content.className = 'scroll-content';
1126
1127					return [panel, content];
1128				},
1129			};
1130
1131			fn[fname]();
1132			return [panel, content];
1133		}
1134
1135		setHandlers() {
1136			document.addEventListener('keydown', e => this.handlers(e, 'onKeyDown'));
1137			this.input.addEventListener('input', () => {
1138				this.searchTags('getSearchResult');
1139			});
1140
1141			// ф-я назначена переменной, чтобы была возможность удалить
1142			// обработчик события
1143			this.handler = e => this.handlers(e, 'onMouseOverItem');
1144			this.ul.addEventListener('mouseover', this.handler);
1145			this.ul.addEventListener('click', e => this.tagsControll(e, 'selectTag'));
1146
1147			this.container.addEventListener('keydown', e => this.keyControl(e, 'onKeyDown'));
1148			this.tagsContainer.addEventListener('click', e => this.tagsControll(e, 'removeTag'));
1149
1150			// this.container.addEventListener('focusout', () => this.handlers(null, 'hideDropPanel'), true);
1151		}
1152
1153		renderTags(fname, name) {
1154			const fn = {
1155				add: () => {
1156					const btn = document.createElement('div');
1157					btn.innerHTML = name;
1158					btn.setAttribute('tag-name', name);
1159					btn.className = 'user-tag ';
1160
1161					const sp = document.createElement('span');
1162					sp.innerHTML = '×';
1163					sp.setAttribute('data-tag', 'delete');
1164					sp.className = 'remove-tag';
1165
1166					btn.append(sp);
1167					this.tagsContainer.append(btn);
1168					this.handlers(null, 'hideDropPanel');
1169				},
1170			};
1171
1172			fn[fname]();
1173		}
1174
1175		handlers(e, fname) {
1176			const fn = {
1177				onKeyDown: () => {
1178					if (e.code !== 'Escape') return;
1179					this.handlers(null, 'hideDropPanel');
1180				},
1181
1182				hideDropPanel: () => {
1183					this.container.classList.remove('active');
1184					this.input.value = '';
1185				},
1186
1187				onMouseOverItem: () => {
1188					let el = e.target;
1189
1190					for (let item of this.items) {
1191						item.classList.remove('current');
1192					}
1193
1194					el.classList.add('current');
1195				},
1196			};
1197
1198			fn[fname]();
1199		}
1200
1201		keyControl(e, fname) {
1202			const fn = {
1203				onKeyDown: () => {
1204					if (e.code && !['ArrowDown', 'ArrowUp', 'Enter', 'NumpadEnter'].includes(e.code)) return;
1205
1206					if (['Enter', 'NumpadEnter'].includes(e.code)) {
1207						this.keyControl(e, 'selectItem');
1208					} else if (['ArrowDown', 'ArrowUp'].includes(e.code)) {
1209						fn.movementItem(e);
1210					}
1211				},
1212
1213				movementItem: (e) => {
1214					e.preventDefault();
1215
1216					this.ul.removeEventListener('mouseover', this.handler);
1217
1218					const i = fn.getIndexForMovement();
1219					if (i === -1) return;
1220
1221					const next = (e.code === 'ArrowDown') ? i + 1 : i - 1;
1222					if (next < 0 || next >= this.items.length) return;
1223
1224					this.items[next].classList.add('current');
1225					if (i >= 0) {
1226						this.items[i].classList.remove('current');
1227					}
1228
1229					this.items[next].scrollIntoView({ block: 'nearest' });
1230
1231					setTimeout(() => this.ul.addEventListener('mouseover', this.handler), 5);
1232				},
1233
1234				selectItem: () => {
1235					e.preventDefault();
1236					const i = fn.getIndexForMovement();
1237					if (i === -1) return;
1238
1239					const el = this.items[i];
1240					if (el.hasAttribute('disabled')) return;
1241
1242					this.tagsControll(null, 'selectTag', el);
1243				},
1244
1245				getIndexForMovement: () => {
1246					return [...this.items].findIndex(item => item.matches('.current'));
1247				},
1248			};
1249
1250			fn[fname]();
1251		}
1252
1253		searchTags(fname, data) {
1254			const fn = {
1255				getSearchResult: () => {
1256					this.str = this.input.value.toLowerCase().trim();
1257					const length = this.str.length;
1258
1259					if (length < MINREQTAG) {
1260						this.container.classList.remove('active');
1261						return;
1262					}
1263
1264					const str = this.str.slice(0, 3);
1265
1266					if (!this.searchArray[str]) {
1267						// получаем от сервера список существующих тегов
1268						const url = `/api/a-getusertags/tagsearch/tagname-${encodeURIComponent(str)}`;
1269						this.serverRequest('getusertags', url, str);
1270					} else {
1271						fn.selectOutputSearchResult(str);
1272					}
1273				},
1274
1275				selectOutputSearchResult: str => {
1276					str = str ? str : data;
1277
1278					this.currArray = this.searchArray[str].filter(tag => tag.name.toLowerCase().includes(this.str));
1279
1280					if (this.currArray.length === 0) {
1281						fn.outputSearchResultEmpty();
1282					} else {
1283						fn.outputSearchResultFromArray();
1284					}
1285				},
1286
1287				disableSearchTags: () => {
1288					const str = this.str.slice(0, 3);
1289					for (let t of this.searchArray[str]) {
1290						if (t.id == data.id) t.disable = 1;
1291					}
1292				},
1293
1294				// вывод результата поиска из сохранённого массива
1295				outputSearchResultFromArray: () => {
1296					if (this.currArray.length === 0) {
1297						fn.outputSearchResultEmpty();
1298						return;
1299					}
1300
1301					const frag = document.createDocumentFragment();
1302					const classname = this.constructor.name;
1303
1304					for (let item of this.currArray) {
1305						if (classname === 'Tags') {
1306							if (item.disable == 1) continue;
1307						}
1308
1309						const li = document.createElement('li');
1310						li.setAttribute('tagid', item.id);
1311						li.innerHTML = item.name;
1312						li.innerHTML = `<div>${this.setHighlights(item.name)}</div>`;
1313
1314						frag.append(li);
1315					}
1316
1317					if (classname === 'Tags') {
1318						let li = fn.createNewTagItem(classname);
1319						if (li) frag.append(li);;
1320					}
1321
1322					this.removeChildren();
1323					this.ul.prepend(frag);
1324
1325					if (!this.ul.firstChild) {
1326						this.container.classList.remove('active');
1327						return;
1328					} else {
1329						this.ul.firstChild.classList.add('current');
1330					}
1331
1332					this.scrollbox.initialize();
1333
1334					this.items = this.ul.children;
1335					this.container.classList.add('active');
1336				},
1337
1338				outputSearchResultEmpty: () => {
1339					const classname = this.constructor.name;
1340					let li = fn.createNewTagItem(classname);
1341
1342					this.removeChildren();
1343					if (li) this.ul.prepend(li);
1344
1345					this.scrollbox.initialize();
1346
1347					this.items = this.ul.children;
1348					this.container.classList.add('active');
1349				},
1350
1351				createNewTagItem: classname => {
1352					const arr = this.currArray.filter(tag => tag.name.toLowerCase() == this.str);
1353					if (arr.length !== 0) return false;
1354
1355					const li = document.createElement('li');
1356					li.setAttribute('tagid', 'new_tag');
1357
1358					if (classname === 'Tags') {
1359						li.innerHTML = '<div>' + lng('Добавить @').replace('@', `&#171;<b>${this.str}</b>&#187; `) + lng('как интерес...') + '</div>';
1360					} else {
1361						li.classList.add('current');
1362						li.innerHTML = '<div>' + lng('Добавить @').replace('@', `&#171;<b>${this.str}</b>&#187;`) + '</div>';
1363					}
1364
1365					return li;
1366				}
1367			};
1368
1369			fn[fname]();
1370		}
1371
1372		tagsControll(e, fname, li) {
1373			const fn = {
1374				selectTag: () => {
1375					let tag = [], name;
1376
1377					let el = e ? e.target : li;
1378					const id = fn.getTagID(el, 'li');
1379
1380					if (!id) return;
1381
1382					if (id == 'new_tag') {
1383						while (!el.matches('li') && (el = el.parentElement));
1384
1385						name = el.querySelector('b').innerHTML;
1386						tag.push({ name });
1387					} else {
1388						tag = this.currArray.filter(tag => tag.id == id);
1389						name = tag[0].name;
1390					}
1391					fn.checkTag(name);
1392				},
1393
1394				removeTag: () => {
1395					const el = e.target;
1396					if (!el.dataset.tag || el.dataset.tag !== 'delete') return;
1397
1398					const tag = el.parentElement;
1399					const name = tag.getAttribute('tag-name');
1400
1401					const i = this.tags.indexOf(name);
1402					if (i === -1) return;
1403
1404					this.tags.splice(i, 1);
1405					fn.updateTagsString();
1406					fn.updateSearchTags(name);
1407
1408					tag.remove();
1409				},
1410
1411				getTagID: (el, parent) => {
1412					while (!el.matches(parent) && (el = el.parentElement));
1413					return (!el.matches(parent)) ? false : el.getAttribute('tagid');
1414				},
1415
1416				updateTagsString: () => {
1417					this.tagsString.value = this.tags.join(';');
1418					this.tagsString.disabled = !this.tagsString.value;
1419				},
1420
1421				updateSearchTags: name => {
1422					if (Object.keys(this.searchArray).length === 0) return;
1423
1424					for (let key in this.searchArray) {
1425						for (let tag of this.searchArray[key]) {
1426							if (tag.name == name) tag.disable = 0;
1427						}
1428					}
1429				},
1430
1431				checkTag: (name) => {
1432					if (this.tags.includes(name)) {
1433						this.handlers(null, 'hideDropPanel');
1434						return;
1435					}
1436
1437					this.tags.push(name);
1438					fn.updateTagsString();
1439					this.renderTags('add', name);
1440				},
1441			};
1442
1443			fn[fname]();
1444		}
1445
1446		serverRequest(fname, url, str) {
1447			const fn = {
1448				getusertags: async () => {
1449					try {
1450						const response = await fetch(url);
1451						const data = await response.json();
1452
1453						this.searchArray[str] = [...data.tags];
1454
1455						this.searchTags('selectOutputSearchResult', str);
1456					} catch (err) {
1457						console.log(err);
1458					}
1459				},
1460			};
1461
1462			fn[fname]();
1463		}
1464
1465		setHighlights(text) {
1466			return text.replace(new RegExp(`${this.str}`, 'i'), '<b>$&</b>');
1467		}
1468
1469		removeChildren(list = this.ul) {
1470			if (list === undefined) return;
1471			while (list.firstChild) {
1472				list.removeChild(list.firstChild);
1473			}
1474		}
1475	};
1476
1477	class Tags extends SearchTags {
1478		constructor(tagscontainer, container, tagtmpl, tags) {
1479			super(tagscontainer, container);
1480
1481			this.tagtmpl = tagtmpl;
1482			this.userTags = tags;
1483			this.p = document.getElementById('notags');
1484
1485			this.renderTags('updateTagsContainer');
1486			super.setHandlers();
1487			this.setHandlers();
1488		}
1489
1490		renderTags(fname, tag, edit = false) {
1491			const fn = {
1492				updateTagsContainer: () => {
1493					this.removeChildren(this.tagsContainer);
1494
1495					this.tmpl = new mustache(this.tagtmpl, {
1496						f_moder: d => {
1497							const tagModeration = [
1498								'not-moderated',
1499								'allowed',
1500								'declined',
1501							];
1502							let moder = d.allowed < 3 ? d.allowed : 2;
1503							return tagModeration[moder];
1504						},
1505					});
1506
1507					for (let tag of this.userTags) {
1508						fn.add(tag);
1509					}
1510					if (this.userTags.length === 0) fn.add({ empty: 1 });
1511				},
1512
1513				add: t => {
1514					tag = t || tag;
1515					tag.edit = edit;
1516					go(this.tagsContainer).append(this.tmpl.parse(tag));
1517				},
1518			};
1519
1520			fn[fname]();
1521		}
1522
1523		setHandlers() {
1524			let el = document.querySelector('#end_edit_tags .btn');
1525			if (el) el.addEventListener('click', () => this.handlers(null, 'hideDropPanel'));
1526		}
1527
1528		tagsControll(e, fname, li) {
1529			const fn = {
1530				selectTag: () => {
1531					let el = e ? e.target : li;
1532					while (!el.matches('li') && (el = el.parentElement));
1533					const id = fn.getTagID(el, 'li');
1534					if (!id) return;
1535
1536					if (id == 'new_tag') {
1537						const name = encodeURIComponent(el.querySelector('b').innerHTML);
1538						const url = `/api/a-newusertags/tags-[{'name':'${name}', 'category':11}]`;
1539						this.serverRequest('newusertags', url);
1540					} else {
1541						const url = `/api/a-addusertags/tagids-[${id}]`;
1542						this.serverRequest('addusertags', url);
1543					}
1544				},
1545
1546				removeTag: () => {
1547					let el = e.target;
1548					if (!el.dataset.tag || el.dataset.tag !== 'delete') return;
1549
1550					const id = fn.getTagID(el, 'div');
1551					if (!id) return;
1552
1553					const url = `/api/a-delusertags/tagids-[${id}]`;
1554					this.serverRequest('delusertags', url, id);
1555				},
1556
1557				getTagID: (el, parent) => {
1558					while (!el.matches(parent) && (el = el.parentElement));
1559					return (!el.matches(parent)) ? false : el.id ? el.id : el.getAttribute('tagid');
1560				},
1561			};
1562
1563			fn[fname]();
1564		}
1565
1566		searchTags(fname, data) {
1567			const fn = {
1568				getSearchResult: () => {
1569					super.searchTags('getSearchResult');
1570				},
1571
1572				// формируем массив с результатами поиска,
1573				// отфильтровывая существующие у юзера теги
1574				filterSearchTags: () => {
1575					const str = this.str.slice(0, 3);
1576					let userTagsID = this.userTags.map(tag => tag.id);
1577					this.searchArray[str] = [];
1578
1579					for (let tag of data.tags) {
1580						if (userTagsID.length !== 0) {
1581							if (userTagsID.includes(tag.id)) tag.disable = 1;
1582						}
1583						this.searchArray[str].push(tag);
1584					}
1585
1586					this.currArray = [...this.searchArray[str]];
1587
1588					super.searchTags('outputSearchResultFromArray');
1589				},
1590
1591				updateSearchTags: () => {
1592					if (Object.keys(this.searchArray).length === 0) return;
1593
1594					for (let key in this.searchArray) {
1595						for (let tag of this.searchArray[key]) {
1596							if (tag.name == data) delete tag.disable;
1597						}
1598					}
1599				},
1600			};
1601
1602			fn[fname]();
1603		}
1604
1605		serverRequest(fname, url, id) {
1606			const fn = {
1607				getusertags: async () => {
1608					try {
1609						const response = await fetch(url);
1610						const data = await response.json();
1611
1612						this.searchTags('filterSearchTags', data);
1613					} catch (err) {
1614						console.log(err);
1615					}
1616				},
1617
1618				addusertags: async () => {
1619					try {
1620						const response = await fetch(url);
1621						const data = await response.json();
1622
1623						const tag = data.tags[0];
1624						tag.search = encodeURIComponent(tag.name);
1625
1626						this.userTags.push(tag);
1627						super.searchTags('disableSearchTags', tag);
1628						this.renderTags('updateTagsContainer', tag, true);
1629
1630						super.handlers(null, 'hideDropPanel');
1631					} catch (err) {
1632						console.log(err);
1633					}
1634				},
1635
1636				delusertags: async () => {
1637					try {
1638						await fetch(url);
1639
1640						const tag = this.userTags.filter(tag => tag.id == id);
1641						this.userTags = this.userTags.filter(tag => tag.id != id);
1642
1643						this.renderTags('updateTagsContainer', tag[0], true);
1644						this.searchTags('updateSearchTags', tag[0].name);
1645					} catch (err) {
1646						console.log(err);
1647					}
1648				},
1649
1650				newusertags: async () => {
1651					try {
1652						const response = await fetch(url);
1653						const data = await response.json();
1654
1655						const tag = data.tags[0];
1656						tag.search = encodeURIComponent(tag.name);
1657
1658
1659						this.userTags.push(tag);
1660						this.renderTags('updateTagsContainer', tag, true);
1661
1662						this.handlers(null, 'hideDropPanel');
1663					} catch (err) {
1664						console.log(err);
1665					}
1666				},
1667			};
1668
1669			fn[fname]();
1670		}
1671	}
1672
1673	class Gallery {
1674		// настройки по-умолчанию
1675		static defaults = {
1676			margin: 10, // расстояние между элементами [px]
1677			visibleItems: 1, // сколько элементов показывать одновременно
1678			param: null, // индекс элемента, с которого начнётся показ
1679			from: null, // на какой странице была вызвана галерея
1680			border: 0, // толщина рамки изображения прописанная в CSS [px]
1681			responsive: true, // адаптивная галерея
1682			autoScroll: false, // автоматическое прокручивание
1683			interval: 3000, // задержка при автоматическом прокручивании [ms]
1684			nav: true, // показать/скрыть кнопки next/prev
1685			dots: true, // показать/скрыть постраничную навигацию
1686			keyControl: false, // управление клавишами вправо / влево
1687			animated: false, // включение анимации
1688			baseTransition: 0.4, // скорость анимации, при изменении CSS свойств
1689			delayTimer: 250, // время задержки при resize страницы [ms]
1690			limit: 30 // ограничиваем перемещение крайних элементов [px]
1691		};
1692
1693		constructor(setup) {
1694			this.gallery = setup.gallery;
1695			this.widthSliderDefault = setup.widthSlider;
1696			this.setup = setup;
1697			this.from = setup.from;
1698
1699			// контейнер в котором отображаются элементы галереи
1700			this.slider = this.gallery.querySelector('.slider');
1701			// контейнер, непосредственно в котором расположены элементы слайдера
1702			this.stage = this.gallery.querySelector('.stage');
1703			// элементы слайдера
1704			this.items = this.gallery.querySelectorAll('.stage > div');
1705			// количество элементов в слайдере
1706			this.count = this.items.length;
1707
1708			this.current = 0; // index координаты текущего элемента
1709			this.next = 0; // index координаты следующего элемента
1710			this.__pressed = false;
1711			this.__events = false;
1712			this.start = 0; // координата, с которой начато перетаскивание
1713			this.shift = 0; // на сколько был перемещён курсор относительно start
1714
1715			setTimeout(() => { this.init(); }, 20);
1716		}
1717
1718		// объединяет значения двух объектов и выдаёт общий результат
1719		static extend(out) {
1720			out = out || {};
1721
1722			for (let i = 1; i < arguments.length; i++) {
1723				if (!arguments[i]) continue;
1724				for (let key in arguments[i]) {
1725					if (arguments[i].hasOwnProperty(key)) out[key] = arguments[i][key];
1726				}
1727			}
1728
1729			console.log('out: ', out);
1730			return out;
1731		};
1732
1733		static xpos(e) {
1734			if (e.targetTouches && (e.targetTouches.length >= 1)) {
1735				return e.targetTouches[0].clientX;
1736			}
1737
1738			// mouse event
1739			return e.clientX;
1740		}
1741
1742		init() {
1743			this.options = Gallery.extend({}, Gallery.defaults, this.setup);
1744
1745			this.setSizeCarousel();
1746			this.setCoordinates();
1747			this.createControl('initControl');
1748
1749			if (this.__events) return;
1750
1751			this.setHandlers();
1752
1753			if (this.options.param) {
1754				this.next = this.options.param * this.options.visibleItems;
1755				const x = this.coordinates[this.next];
1756
1757				this.scroll(x, 0);
1758
1759				this.options.param = null;
1760			}
1761		}
1762
1763		setSizeCarousel() {
1764			// вьюпорт, в котором прокручиваются элементы галереи
1765			this.widthSlider = this.slider.offsetWidth || this.widthSliderDefault;
1766
1767			if (this.options.responsive) this.setAdaptiveOptions();
1768
1769			this.max = this.count - this.options.visibleItems;
1770
1771			// ширина элемента слайдера
1772			const w = (this.widthSlider - this.options.margin * (this.options.visibleItems - 1)) / this.options.visibleItems;
1773
1774			this.width = w + this.options.margin;
1775			this.widths = this.width * this.count;
1776
1777			this.stage.style.width = this.widths + 'px';
1778
1779			for (let [index, item] of this.items.entries()) {
1780				if (this.from && item.hasAttribute('data-href') && typeof evproc != 'undefined') {
1781					item.addEventListener('click', e => {
1782						evproc.fb('story_click', { from: this.from, story_number: index + 1 });
1783					});
1784
1785					item.dataset.href = `${item.dataset.href}/from-${this.from}`;
1786				}
1787
1788				item.style.cssText = `width:${w}px; margin-right:${this.options.margin}px;`;
1789			}
1790
1791			// после того, как каркас галереи построен, все размеры элементов
1792			// вычислены и прописаны в стилях, делаем карусель видимой через
1793			// свойство стиля 'visibility'
1794			setTimeout(() => { this.gallery.style.visibility = 'visible'; }, 180);
1795		}
1796
1797		setCoordinates() {
1798			let point = 0;
1799			this.coordinates = [];
1800
1801			while (this.coordinates.length < this.count) {
1802				this.coordinates.push(point);
1803				point -= this.width;
1804			}
1805		}
1806
1807		setAdaptiveOptions() {
1808			// размер видимой части окна браузера
1809			const width = document.documentElement.clientWidth;
1810			// массив с контрольными точками
1811			const points = [];
1812			// текущая контрольная точка
1813			let point;
1814
1815			for (let key in this.options.adaptive) {
1816				points.push(key);
1817			}
1818
1819			for (let i = 0, j = points.length; i < j; i++) {
1820				let a = points[i],
1821					b = (points[i + 1] !== undefined) ? points[i + 1] : points[i];
1822
1823				if (width <= points[0]) {
1824					point = points[0];
1825				} else if (width >= a && width < b) {
1826					point = a;
1827				} else if (width >= points[points.length - 1]) {
1828					point = points[points.length - 1];
1829				}
1830			}
1831
1832			const setting = this.options.adaptive[point];
1833			for (let key in setting) {
1834				this.options[key] = setting[key];
1835			}
1836		}
1837
1838		createControl(fname) {
1839			const fn = {
1840				initControl: () => {
1841					this.navCtrl = this.gallery.querySelector('.nav-ctrl');
1842					this.dotsCtrl = this.gallery.querySelector('.dots-ctrl');
1843
1844					if (this.options.nav === true) {
1845						this.btnPrev = this.navCtrl.querySelector('[data-shift=prev]');
1846						this.btnNext = this.navCtrl.querySelector('[data-shift=next]');
1847
1848						fn.setNavStyle();
1849
1850						this.navCtrl.dataset.hidden = false;
1851					} else {
1852						this.navCtrl.dataset.hidden = true;
1853					}
1854
1855					if (this.options.dots === true) {
1856						fn.creatDotsCtrl();
1857						this.dotsCtrl.dataset.hidden = false;
1858					} else {
1859						this.dotsCtrl.dataset.hidden = true;
1860					}
1861				},
1862
1863				setNavStyle: () => {
1864					this.btnPrev.classList.remove('disable');
1865					this.btnNext.classList.remove('disable');
1866
1867					if (this.current === 0) {
1868						this.btnPrev.classList.add('disable');
1869					} else if (this.current >= this.count - this.options.visibleItems) {
1870						this.btnNext.classList.add('disable');
1871					}
1872				},
1873
1874				creatDotsCtrl: () => {
1875					this.spots = [];
1876					this.dotsCtrl.innerHTML = '';
1877
1878					const frag = document.createDocumentFragment();
1879					let i = 0, point = 0;
1880
1881					while (i < this.count) {
1882						const li = document.createElement('li');
1883						frag.append(li);
1884						this.spots.push(li);
1885
1886						i += this.options.visibleItems;
1887						point = (i <= this.max) ? point - this.width * this.options.visibleItems : -this.width * this.max;
1888					}
1889
1890					this.dotsCtrl.prepend(frag);
1891					fn.setDotsStyle();
1892				},
1893
1894				setDotsStyle: () => {
1895					for (let spot of this.spots) {
1896						spot.classList.remove('active');
1897					}
1898
1899					let i = this.options.param ? this.options.param : (this.next < this.max) ? Math.trunc(this.next / this.options.visibleItems) : this.spots.length - 1;
1900					this.spots[i].classList.add('active');
1901				},
1902			};
1903
1904			fn[fname]();
1905		}
1906
1907		setHandlers(e) {
1908			window.addEventListener('resize', () => this.handlers(null, 'resize'));
1909			if (this.options.autoScroll) this.handlers(null, 'autoScroll');
1910			this.navCtrl.addEventListener('click', e => this.handlers(e, 'navControl'));
1911			this.dotsCtrl.addEventListener('click', e => this.handlers(e, 'dotsControl'));
1912			if (this.options.keyControl) {
1913				window.addEventListener('keydown', e => this.handlers(e, 'keyControl'));
1914			}
1915
1916			// mouse events
1917			// только, если указатель мыши находится над галереей
1918			this.gallery.querySelector('.slider').addEventListener('wheel', e => this.handlers(e, 'wheelControl'));
1919
1920			// touch events
1921			this.stage.addEventListener('touchstart', e => this.handlers(e, 'tap'));
1922			this.stage.addEventListener('touchmove', e => this.handlers(e, 'drag'));
1923			this.stage.addEventListener('touchend', e => this.handlers(e, 'release'));
1924
1925			this.__events = true;
1926		}
1927
1928		handlers(e, fname) {
1929			const fn = {
1930				resize: () => {
1931					clearTimeout(this.resizeTimer);
1932					this.resizeTimer = setTimeout(() => {
1933						this.init();
1934
1935						this.current = (this.current <= this.max) ? this.current : this.max;
1936
1937						let x = this.coordinates[this.current];
1938						this.scroll(x, this.options.baseTransition);
1939					}, this.options.delayTimer);
1940				},
1941
1942				autoScroll: () => {
1943					let tm = setInterval(() => {
1944						let x = this.getNextCoordinates(1);
1945						if (this.next == this.count - 1) {
1946							this.options.autoScroll = false;
1947							clearInterval(tm);
1948						};
1949						this.scroll(x, this.options.baseTransition);
1950					}, this.options.interval);
1951				},
1952
1953				navControl: () => {
1954					const el = e.target;
1955					if (!el.matches('span')) return;
1956
1957					// -1 - prev, 1 - next
1958					const d = (el.dataset.shift === 'next') ? 1 : -1;
1959					const x = this.getNextCoordinates(d);
1960
1961					this.scroll(x, this.options.baseTransition);
1962				},
1963
1964				dotsControl: () => {
1965					const el = e.target;
1966
1967					if (!el.matches('li') || el.classList.contains('active')) return;
1968
1969					const i = this.spots.indexOf(el);
1970					if (i == -1) return;
1971
1972					this.next = i * this.options.visibleItems;
1973					this.next = (this.next <= this.max) ? this.next : this.max;
1974
1975					const x = this.coordinates[this.next];
1976					const n = Math.abs(this.current - this.next);
1977					const t = this.options.baseTransition + n * 0.07;
1978
1979					this.scroll(x, t);
1980				},
1981
1982				keyControl: () => {
1983					if (e.code && !['ArrowRight', 'ArrowLeft']) return;
1984					const d = (e.code === 'ArrowRight') ? 1 : -1;
1985					const x = this.getNextCoordinates(d);
1986
1987					this.scroll(x, this.options.baseTransition);
1988				},
1989
1990				wheelControl: () => {
1991					e.preventDefault();
1992					const d = (e.deltaY > 0) ? 1 : -1;
1993					const x = this.getNextCoordinates(d);
1994
1995					this.scroll(x, this.options.baseTransition);
1996				},
1997
1998				tap: () => {
1999					// e.preventDefault();
2000					e.stopPropagation();
2001
2002					this.start = Gallery.xpos(e);
2003					this.shift = 0;
2004					this.__pressed = true;
2005				},
2006
2007				drag: () => {
2008					e.preventDefault();
2009					e.stopPropagation();
2010
2011					if (this.__pressed === false) return;
2012
2013					this.shift = this.start - Gallery.xpos(e);
2014
2015					if (Math.abs(this.shift) < 3) return;
2016					if (this.current === 0 && this.shift < 0 || this.current == this.max && this.shift > 0) return;
2017
2018					const remaining = this.widths - this.width * this.options.visibleItems;
2019					const delta = this.coordinates[this.current] - this.shift;
2020
2021					if (delta >= 0 || Math.abs(delta) - remaining >= 0) return;
2022
2023					this.scroll(delta, 0);
2024				},
2025
2026				release: () => {
2027					if (Math.abs(this.shift) >= 3) {
2028						e.preventDefault();
2029						e.stopPropagation();
2030					}
2031
2032					if (this.__pressed === false) return;
2033
2034					let d = null;
2035					if (Math.abs(this.shift) > this.width / 10) {
2036						d = this.shift > 0 ? Math.ceil(this.shift / this.width) : Math.floor(this.shift / this.width);
2037					}
2038
2039					const x = this.getNextCoordinates(d);
2040
2041					this.scroll(x, this.options.baseTransition);
2042
2043					this.__pressed = false;
2044				},
2045			};
2046
2047			fn[fname]();
2048		}
2049
2050		/* Прокручивание галереи */
2051		getNextCoordinates(direction) {
2052			if (!direction) return this.coordinates[this.current];
2053
2054			// direction - направление перемещения: -1 - left, 1 - right
2055			if (this.options.autoScroll && this.current >= this.count - this.options.visibleItems) {
2056				this.next = 0;
2057			} else {
2058				if (this.current === 0 && direction <= -1 || (this.current >= this.max) && direction >= 1) return;
2059				this.next += direction;
2060				this.next = (this.next > this.max) ? this.max : (this.next < 0) ? 0 : this.next;
2061			}
2062
2063			return this.coordinates[this.next];
2064		}
2065
2066		scroll(x, transition) {
2067			if (typeof (x) !== 'number') return;
2068
2069			this.stage.style.cssText = `width:${this.widths}px; transform:translateX(${x}px); transition:${transition}s`;
2070
2071			this.current = (this.next < this.max) ? this.next : this.max;
2072
2073			if (this.options.nav) this.createControl('setNavStyle');
2074			if (this.options.dots) this.createControl('setDotsStyle');
2075		}
2076	};
2077
2078	// Инициализация контроллеров
2079
2080	function controllInitalize() {
2081		initSelect();
2082	}
2083
2084	function controllRefrash() {
2085		reinitSelect();
2086	}
2087
2088	function controllSlider(setup) {
2089		new Slider(setup);
2090	}
2091
2092	function controllSearchTags(tagscontainer, container, tagsString) {
2093		new SearchTags(tagscontainer, container, tagsString);
2094	}
2095
2096	function controllTags(tagscontainer, container, tagtmpl, tags) {
2097		new Tags(tagscontainer, container, tagtmpl, tags);
2098	}
2099
2100	function controllGallery(setup) {
2101		new Gallery(setup);
2102	}
2103
2104	return { controllInitalize, controllRefrash, controllSlider, controllTags, controllSearchTags, controllGallery };
2105})();

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.