· 6 years ago · Mar 29, 2020, 09:16 PM
1/*
2*
3* Методы класса Assignment
4*
5*/
6window.theOneAssignmentMethods = {
7};
8
9
10
11
12
13
14
15/*
16*
17* Методы класса Task
18*
19*/
20window.theOneTaskPrototype = {
21 //Тут проводятся все манипуляции со входной спекой и ШВшными JSONками.
22 getTemplateData: function () {
23 window.currentRenderingTask = this;
24 if (!this.theOneSolution) {
25 this.theOneSolution = {
26 __theOneCustomStorage: {
27 __actionLog: [],
28 __timingsStorage: {total: 0},
29 __uploadingFiles: {}
30 }
31 }
32 }
33
34 let templateData = TolokaHandlebarsTask.prototype.getTemplateData.call(this);
35
36 //Объект, в котором будут храниться все обработчики событий для элементов из JSONок
37 this.elementEventHandlers = {};
38
39 //Установка флагов режима только для чтения и просмотра выполненных заданий
40 if (this.getWorkspaceOptions().isReadOnly) {
41 this.isReadOnly = true;
42 }
43 if (this.getWorkspaceOptions().isReviewMode) {
44 this.isReviewMode = true;
45 }
46
47 //Установка мобильного флага для Handlebars
48 if (this.getWorkspaceOptions().agent !== 'FRONTEND') {
49 templateData.thisIsMobileDevice = true;
50 window.thisIsMobileDevice = true;
51 }
52
53 //Если это режим просмотра выполненных заданий - нужно позаботиться о том, чтобы в шаблоне отображалась соответствующая солюшке картина
54 if (this.isReviewMode || this.isReadOnly) {
55 if (usingOneRawData && this.getSolution().output_values.__theOneRawData) {
56 this.startingSolution = JSON.parse(this.getSolution().output_values.__theOneRawData);
57 if (printSolution) {
58 console.log('Стартовая солюшка из __theOneRawData: ', this.startingSolution);
59 }
60 } else {
61 this.startingSolution = this.flattenSolution(this.getSolution().output_values);
62 if (printSolution) {
63 console.log('Стартовая солюшка: ', this.getSolution().output_values, ', преобразована в: ', this.startingSolution);
64 }
65 }
66 }
67
68 templateData = setTemplateData.apply(this, [templateData]);
69
70 return templateData;
71 },
72
73 //Распрямляет солюшку, превращая её в одноэтажный объект. Это чтобы когда солюшка со сложной структурой пришла на вход, ШВ мог из неё вытянуть состояние всех своих контролов.
74 //Вам сюда лезть не надо.
75 flattenSolution: function (solution) {
76 let flatSolution = {};
77
78 function addToFlat(o, name) {
79 if (Object.prototype.toString.call(o) === "[object Object]") {
80 for (let subO in o) {
81 addToFlat(o[subO], subO);
82 }
83 } else {
84 flatSolution[name] = o;
85 }
86 }
87
88 for (let o in solution) {
89 addToFlat(solution[o], o);
90 }
91
92 //Если одно и то же поле маппится в несколько разных - предпочтение отдаем тому, которое маппится без функции.
93 const thereWasAFunction=[];
94
95 if (useSpecMapper) {
96 for (const prop in solution) {
97 if (specMapper[prop] && specMapper[prop].from) {
98 const fromNames = Array.isArray(specMapper[prop].from) ? specMapper[prop].from : [specMapper[prop].from];
99 for (const origName of fromNames) {
100 if (!flatSolution[origName] || (!Array.isArray(flatSolution[origName]) && thereWasAFunction[origName] && !specMapper[prop].f && !specMapper[prop].function)) {
101 flatSolution[origName] = solution[prop];
102 thereWasAFunction[origName] = specMapper[prop].f || specMapper[prop].function;
103 } else if (Array.isArray(flatSolution[origName]) && Array.isArray(solution[prop])) {
104 for (const e of solution[prop]) {
105 if (!flatSolution[origName].includes(e)) {
106 flatSolution[origName].push(e);
107 }
108 }
109 }
110 }
111 }
112 }
113 }
114
115 return flatSolution;
116 },
117
118 //"Придумывает" уникальное в рамках таска имя для чего-нибудь
119 comeUpWithAnUniqueName: function (justNumber) {
120 if (!this.uniqueNameCounter) {
121 this.uniqueNameCounter = 0;
122 }
123 return "${justNumber ? '' : 'uniqueAutoName'}${this.uniqueNameCounter++}";
124 },
125
126 //Для случаев, когда для имён нужно получить слаги, а потом сконвертировать обратно в имена - эти две функции
127 getSlugForName: function (name) {
128 const
129 dictionary = this.getStorageItem('__uniqueSlugsDictionary') || {};
130
131 for (const slug in dictionary) {
132 if (dictionary[slug] === name) {
133 return slug;
134 }
135 }
136
137 const slug = this.comeUpWithAnUniqueName();
138 dictionary[slug] = name;
139 this.setStorageItem('__uniqueSlugsDictionary', dictionary);
140 return slug;
141 },
142 getNameForSlug: function(slug) {
143 const
144 dictionary = this.getStorageItem('__uniqueSlugsDictionary') || {};
145
146 return dictionary[slug] || false;
147 },
148
149 //Парсит входную JSONку и делает с ней необходимые операции прежде чем отдать её во входную спеку.
150 //Вам сюда тоже лезть не надо.
151 getJSON: function (JSON, namesPostfix) {
152 const task = this, json =/*stringified*/JSON, postfix = '__clone_' + namesPostfix;
153
154 const getElementByName = function (name) {
155 for (const e of this) {
156 if (e.name && e.name === name) {
157 return e;
158 }
159 }
160 return false;
161 };
162
163 const getElementIndexByName = function (name) {
164 for (const i in this) {
165 if (this[i].name && this[i].name === name) {
166 return i;
167 }
168 }
169 return false;
170 };
171
172 const setPropValue = function (propName, value) {
173 this[propName] = value;
174 };
175
176 function initEventHandlers(element, fNames, namePrefix) {
177 let eHandlers = {};
178
179 for (const hName of fNames) {
180 if (element[hName] && typeof element[hName] == 'function') {
181 eHandlers[hName] = element[hName];
182 }
183 }
184
185 if (Object.keys(eHandlers)) {
186 if (!element.name) {
187 element.name = task.comeUpWithAnUniqueName();
188 }
189 task.elementEventHandlers[namePrefix ? "${namePrefix}___${element.name}" : element.name] = eHandlers;
190 }
191 }
192
193 function _initJSONArray(json, isRecursive) {
194 Array.from(json).forEach(element => {
195
196 if (!element.name) {
197 element.name = task.comeUpWithAnUniqueName();
198 }
199
200 //Первым делом добавляем к имени постфикс, если он есть (когда мы в setTemplateData множим какую-то джсонку)
201 if (namesPostfix || namesPostfix === 0) {
202 element.name += postfix;
203 }
204
205 //Еслим мы в режиме просмотра размеченного задания и есть солюшка
206 if (!isRecursive && task.startingSolution) {
207 element = task.prepareJSONElement(element);
208 }
209
210 if (element.type && element.type === 'group') {
211
212 initJSONArray(element.groupFields, 'recursive');
213
214 //Функция получения порядкового номера элемента по имени
215 element.groupFields.getElementIndexByName = getElementIndexByName;
216
217 //Функция получения элемента по имени
218 element.groupFields.getElementByName = getElementByName;
219
220 } else {
221
222 //В целях оптимизации запоминаем, в каких именно элементах JSONки спеки содержится элемент с данным именем
223 task.theOneSpec.specByName[element.name] = JSON;
224
225 //Функция установки свойству определённого значения
226 element.setPropValue = setPropValue;
227
228 //Если это элемент с массивом кнопок
229 if (element.buttons) {
230
231 //Функция получения кнопки по имени
232 element.buttons.getByName = getElementByName;
233
234 //Функция получения порядкового номера кнопки по имени
235 element.buttons.getIndexByName = getElementIndexByName;
236
237 //Функция добавления кнопки элементу в конец
238 element.buttons.add = function (buttJSON) {
239 buttJSON.setPropValue = setPropValue;
240 initEventHandlers(buttJSON, ['onClick', 'onActivation', 'onDeactivation'], element.name);
241 element.buttons.push(buttJSON);
242 };
243
244 //Функция добавления кнопки элементу в начало
245 element.buttons.addToStart = function (buttJSON) {
246 buttJSON.setPropValue = setPropValue;
247 element.buttons.unshift(buttJSON);
248 };
249
250 //Функция удаления кнопки у элемента по имени или индексу
251 element.buttons.remove = function (nameOrIndex) {
252 let index;
253 if (typeof nameOrIndex == "string") {
254 index = element.buttons.getButtonIndexByName(nameOrIndex);
255 if (!index) {
256 return;
257 }
258 } else {
259 index = nameOrIndex;
260 }
261
262 delete task.elementEventHandlers[this[index].name];
263
264 element.buttons.splice(index, 1);
265 };
266
267
268 //Проверяем кнопки на наличие обработчиков, и при необходимости обрабатываем их
269 for (const butt of element.buttons) {
270
271 if (!butt.name) {
272 butt.name = task.comeUpWithAnUniqueName();
273 }
274
275 //Добавляем указанный постфикс ссылкам на другие элементы, которым мы уже постфикс добавили
276 if (namesPostfix || namesPostfix === 0) {
277 if (butt.properties) {
278 for (const propName of ['includes', 'excludes']) {
279 if (butt.properties[propName]) {
280 for (const e in butt.properties[propName]) {
281 butt.properties[propName][e] += postfix;
282 }
283 }
284 }
285 }
286 }
287
288 //Функция установки свойству определённого значения
289 butt.setPropValue = setPropValue;
290
291 initEventHandlers(butt, ['onClick', 'onActivation', 'onDeactivation'], element.name);
292
293 }
294 }
295
296 //Обработка инпутов и текстарей
297 if (['text', 'textarea', 'integer', 'float', 'link', 'date', 'email'].includes(element.type)) {
298 initEventHandlers(element, ['onInput', 'validation']);
299 }
300
301 }
302 });
303 }
304
305 const initJSONArray = _initJSONArray.bind(task);
306
307 initJSONArray(json);
308
309 //Функция получения порядкового номера элемента по имени
310 json.getElementIndexByName = getElementIndexByName;
311
312 //Функция получения элемента по имени
313 json.getElementByName = getElementByName;
314
315 return json;
316 },
317
318 //Рекурсивная функция для подготовки входной JSONки. Если на вход пришла солюшка - раскидывает значения из неё по описанным в JSONке элементам.
319 //Вам сюда уж точно лезть не нужно.
320 prepareJSONElement: function (e, solution) {
321 let task = this;
322
323 if (!solution) {
324 solution = task.startingSolution;
325 }
326
327 if (!e.type) {
328 return e;
329 }
330
331 function putSelectedToFlags(flags) {
332 if (!flags) {
333 flags = [];
334 }
335 if (!flags.includes('selected')) {
336 flags.push('selected');
337 }
338 return flags;
339 }
340
341 function removeSelectedFromFlags(flags) {
342 if (flags && flags.includes('selected')) {
343 flags.splice(flags.indexOf('selected'), 1);
344 }
345 return flags;
346 }
347
348 switch (e.type) {
349 case 'date': //Поле со вводом даты. Обычный инпут.
350 case 'text': //Поле со вводом текста. Обычный инпут.
351 case 'email': //Поле со вводом мыла. Обычный инпут.
352 case 'link': //Поле со вводом ссылки. Обычный инпут.
353 case 'integer': //Поле со вводом целого числа. Обычный инпут.
354 case 'float': //Поле со вводом дробного числа. Обычный инпут.
355 case 'textarea':
356 if (solution[e.name]) {
357 e.value = escapeHTML(solution[e.name]);
358 } else {
359 e.value = '';
360 }
361 break;
362 case 'switchButton':
363 if (solution[e.name]) {
364 e.flags = putSelectedToFlags(e.flags);
365 } else {
366 e.flags = removeSelectedFromFlags(e.flags);
367 }
368 break;
369 case 'radioGroup':
370 e.buttons.forEach(butt => {
371 if (butt.name === solution[e.name]) {
372 butt.flags = putSelectedToFlags(butt.flags);
373 } else {
374 butt.flags = removeSelectedFromFlags(butt.flags);
375 }
376 });
377 break;
378 case 'checkboxGroup':
379 e.buttons.forEach(butt => {
380 if (solution[e.name] && solution[e.name].includes(butt.name)) {
381 butt.flags = putSelectedToFlags(butt.flags);
382 } else {
383 butt.flags = removeSelectedFromFlags(butt.flags);
384 }
385 });
386 break;
387 case 'buttonGroup':
388 e.buttons.forEach(butt => {
389 if (solution[butt.name]) {
390 butt.flags = putSelectedToFlags(butt.flags);
391 } else {
392 butt.flags = removeSelectedFromFlags(butt.flags);
393 }
394 });
395 break;
396 case 'group':
397 if (e.name && solution[e.name] && e.flags && e.flags.includes('infinite')) {
398 //Обработка "бесконечных" групп
399 let newGroupFields = [];
400 Array.from(solution[e.name]).forEach(sol => {
401 e.groupFields.forEach(groupField => {
402 let newField = $.extend(true, {}, groupField);
403 newField = task.prepareJSONElement(newField, sol);
404 newGroupFields.push(newField);
405 });
406 });
407 e.groupFields = newGroupFields;
408 } else {
409 //Обработка обычных групп
410 e.groupFields.forEach(groupField => {
411 groupField = task.prepareJSONElement(groupField);
412 });
413 }
414 break;
415 }
416
417 return e;
418 },
419
420 //Классический Толоковский онРендер. Дополняйте как хотите после комментариев "Кастомные штуки" в зависимости от того, когда именно должен выполниться ваш код.
421 onRender: function () {
422 const root = this.getDOMElement(), task = this;
423 this.errorMSGs = [];
424
425 //Инициализируем модальные окна
426 this.initModalWindows();
427
428 //Биндим события на статические элементы
429 this.initFieldsHere(root);
430
431 //Инициализация кастомных элементов заголовка
432 if (window.customTitleElements) {
433 for (const e of customTitleElements) {
434 if (e.onRender) {
435 e.onRender.apply(this, [root.querySelector('.blockTitle.' + e.name)]);
436 }
437 }
438 }
439
440 //[Директ] - если чел кликал по ссылке в баннере последние 24 часа - убираем анкликед и прописываем визитед
441 if (!notForDirect && this.storage) checkDomain: {
442 const link = root.querySelector('.bannerTitle a');
443 if (!link) {
444 break checkDomain;
445 }
446 const domain = link.dataset.domain;
447 if (!domain) {
448 break checkDomain;
449 }
450
451 const result = this.storage.getItem('funfrogDirectDomainVisited_' + domain);
452
453 if (result) {
454 link.parentElement.classList.add('visited');
455 link.classList.remove('unclicked');
456 } else {
457 link.addEventListener('click', function () {
458 link.parentElement.classList.add('visited');
459 task.storage.setItem('funfrogDirectDomainVisited_' + domain, true);
460
461 //Теперь надо перелопатить все ссылки на этот домен во всем сьюте и им тоже прописать visited и убрать анкликед
462 for (const otherLink of root.parentElement.querySelectorAll('.bannerTitle a')) {
463 if (otherLink.dataset.domain !== domain) {
464 continue;
465 }
466 otherLink.parentElement.classList.add('visited');
467 otherLink.classList.remove('unclicked');
468 task.turnOffErrorClassOnParentElementTagBlock(otherLink);
469 }
470 });
471 }
472 }
473
474 //На все некликанные ссылки вешаем обработчик, чтобы они стали кликанными при кликании по ним
475 for (const link of root.querySelectorAll('a.unclicked')) {
476 link.addEventListener('click', function () {
477 task.turnOffErrorClassOnParentElementTagBlock(this);
478 this.classList.remove('unclicked');
479 });
480 }
481
482 //Контекстные менюшки
483 if (contextMenu && customContextMenu) {
484 root.addEventListener('contextmenu', (e) => {
485 this.handleContextMenuOpen(e);
486 });
487 }
488
489 document.ready.then(() => {
490 //Если по чему-то надо кликать-кликаем
491 Array.from(root.querySelectorAll('.toBeClickedAtStart')).forEach(function (clickTarget) {
492 try {
493 task.getAssignment().getTaskSuite().focusElement(clickTarget.dataset.number);
494 } catch {
495 clickTarget.click();
496 }
497 clickTarget.classList.remove('toBeClickedAtStart');
498 });
499
500 //Если на старте есть солюшка - применяем её
501 if (!task.startingSolution && Object.keys(this.theOneSolution).length > 1) {
502 this.loadSolution(this.theOneSolution);
503 }
504
505 //Если у нас режим просмотра выполенных заданий - удаляем все пустые бесконечные группы
506 if (this.isReadOnly) {
507 const emptyGroups = root.querySelectorAll('.groupContainer.infinite>.group:not(:first-child):not(.hasSomething)');
508 if (emptyGroups) {
509 for (let i = 0; i < emptyGroups.length; i++) {
510 emptyGroups[i].parentElement.removeChild(emptyGroups[i]);
511 }
512 }
513 }
514
515 task.rendered = true;
516
517 //Кастомные штуки
518 if (window.onTaskReady) {onTaskReady.apply(task, [root]);}
519 }).catch(err => console.error(err));
520
521 //Кастомные штуки
522 if (window.onTaskRender) {onTaskRender.apply(this, [root]);}
523 },
524
525 //Закрывает ШВшные контекстные меню
526 closeAllContextMenus: function() {
527 const
528 root = this.getDOMElement();
529 for (const oldMenu of root.querySelectorAll('.customContextMenu')) {
530 oldMenu.parentElement.removeChild(oldMenu);
531 }
532 this.customContextMenuOpened = false;
533 },
534
535 //Открывает контекстное меню
536 handleContextMenuOpen: function(event) {
537 const
538 menuScreenMargin = 20,
539 root = this.getDOMElement();
540
541 //Make sure ни одно контекстное меню не открыто
542 this.closeAllContextMenus();
543
544 if (event.target.closest('.customContextMenuItemsContainer')) {
545 return;
546 }
547
548 const menu = document.createElement('div');
549 menu.className = 'customContextMenu';
550 menu.innerHTML = '<div class="customContextMenuItemsContainer alien"><div class="customContextMenuRightClickField">Правый клик сюда<br>откроет меню браузера</div></div>';
551 const itemsContainer = menu.querySelector('.customContextMenuItemsContainer');
552 const itemsArray = [];
553
554 const addItem = (item) => {
555 if (item.isVisible && !item.isVisible.apply(this, [])) {return;}
556 itemsArray.push(item);
557 };
558
559 for (const itemName of customContextMenu.items) {
560 if (standardContextMenuItems[itemName]) {
561 addItem(standardContextMenuItems[itemName]);
562 }
563 }
564
565 for (const item of customContextMenu.customItems) {
566 addItem(item);
567 }
568
569 if (itemsArray.length < 1) {return;}
570 event.preventDefault();
571 this.customContextMenuOpened = true;
572 const handleRootClick = (e) => {
573 if (!e.target.closest('.customContextMenu')) {
574 this.closeAllContextMenus();
575 root.removeEventListener('click', handleRootClick);
576 }
577 };
578 root.addEventListener('click', handleRootClick);
579
580 itemsContainer.innerHTML += getFieldsHTML(this.getJSON([
581 {
582 type: "actionButtonGroup",
583 name: "customContextMenuActionButtons",
584 flags: ["block", "unfocusable"],
585 buttons: itemsArray
586 }
587 ]));
588 root.appendChild(menu);
589 this.initFieldsHere(itemsContainer);
590
591 //console.log(event);
592
593 const task = fullScreenMode ? event.target.closest('.block.main') || event.target.closest('.windowContent') || event.target.closest('.task') : event.target.closest('.windowContent') || event.target.closest('.task');
594
595 if (!task) {
596 menu.style.left = event.clientX + 'px';
597 menu.style.top = event.clientY + 'px';
598 } else {
599 const
600 menuRect = menu.getBoundingClientRect(),
601 taskRect = task.getBoundingClientRect();
602
603 if (menuRect.width + event.clientX + menuScreenMargin > taskRect.width) {
604 menu.style.left = event.clientX - menuRect.width + 'px';
605 } else {
606 menu.style.left = event.clientX + 'px';
607 }
608
609 if (menuRect.height + event.clientY + menuScreenMargin > taskRect.height) {
610 menu.style.top = event.clientY - menuRect.height + 'px';
611 } else {
612 menu.style.top = event.clientY + 'px';
613 }
614 }
615 },
616
617 //Инициализация всех используемых в шаблоне модальных окон
618 initModalWindows: function () {
619 const root = this.getDOMElement(), task = this, _window = window;
620 this.windows = {};
621 this.activeWindowsChain = []; //Все открытые в данный момент окна, в порядке открытия
622
623 if (!window.modalWindows) {
624 window.modalWindows = {};
625 }
626
627 //Добавляем ШВшные окна
628 modalWindows._fileWithSameNameAlreadyUploadedMessageWindow = {
629 title: 'Ой!',
630 content: "Файл с таким именем уже был загружен!",
631 buttons: [
632 {
633 caption: 'Понятно.',
634 f(window) {
635 window.close();
636 },
637 }
638 ]
639 };
640
641 for (const windowName in modalWindows) {
642 const windowObj = modalWindows[windowName];
643 let window = root.querySelector('.window.' + windowName);
644
645 if (!window) { //Окно не описали в Handlebars - значит сами его создадим
646 const div = document.createElement('div');
647 div.innerHTML = this.compileHandlebars("{{#window name="${windowName}"}}{{/window}}");
648 window = div.querySelector('.window.' + windowName);
649 root.appendChild(window);
650
651 }
652
653 if (windowObj.flags && windowObj.flags.includes('temporary')) {
654 window.isTemporary = true;
655 }
656
657 window.className += flagsToClassName(windowObj.flags);
658
659 window.open = function () {
660 window.classList.add('opened');
661 _window.activeWindow = window;
662 task.activeWindowsChain.push(window);
663 window.content.scrollTop = 0;
664
665 if (window.isTemporary) {
666 window.setContent(windowObj.content);
667 task.initFieldsHere(window.content);
668 }
669
670 //Обновлянем все просмотрщики с канвасами
671 for (const c of window.querySelectorAll('.imageField .imageBlock canvas')) {
672 if (c.initCanvas) {
673 c.initCanvas();
674 }
675 }
676
677 clearTimeout(_window.focusTimeout);
678 _window.focusTimeout = setTimeout(function () {
679 task.getAssignment().getTaskSuite().focusNextElement(!!(windowObj.flags && windowObj.flags.includes('noScrollAfterOpening')));
680 }, 100);
681
682 if (window.onOpen) window.onOpen();
683
684 return new Promise(((resolve, reject) => {
685 window.resolve = (obj) => {
686 window.isResolved = true;
687 resolve(obj);
688 };
689 window.reject = (obj) => {
690 reject(obj);
691 };
692 }));
693 };
694
695 window.close = function () {
696 window.classList.remove('opened');
697 task.activeWindowsChain.splice(task.activeWindowsChain.indexOf(window), 1);
698 _window.activeWindow = task.activeWindowsChain[task.activeWindowsChain.length - 1];
699
700 clearTimeout(_window.focusTimeout);
701 _window.focusTimeout = setTimeout(function () {
702 task.getAssignment().getTaskSuite().focusNextElement(!!(windowObj.flags && windowObj.flags.includes('noScrollAfterClosing')));
703 }, 100);
704
705 if (window.onClose) window.onClose();
706
707 if (window.isTemporary) {
708 window.setContent('');
709 }
710
711 if (!window.isResolved && window.reject) {
712 window.reject();
713 }
714
715 delete window.isResolved;
716 };
717
718 if (windowObj.onOpen) window.onOpen = function () {
719 windowObj.onOpen.apply(task, [window]);
720 };
721 if (windowObj.onRender) window.onRender = function () {
722 windowObj.onRender.apply(task, [window]);
723 };
724 if (windowObj.onClose) window.onClose = function () {
725 windowObj.onClose.apply(task, [window]);
726 };
727
728 let buttonsHTML = '';
729 if (windowObj.buttons) {
730 let buttonsJSON = {
731 type: "actionButtonGroup",
732 class: "noIcon windowButtons_" + windowName,
733 buttons: []
734 };
735
736 let i = 0;
737 for (const butt of windowObj.buttons) {
738 butt.number = i++;
739 buttonsJSON.buttons.push(
740 {
741 iconClass: 'winButton' + butt.number,
742 caption: butt.caption,
743 name: butt.name
744 }
745 );
746 }
747
748 buttonsHTML = handleFieldAndGetHTML.apply(this, [buttonsJSON]);
749 }
750
751 window.innerHTML = "<div class="windowBox block"><div class='blockTitle'></div><div class="windowContent answerBlock">${window.innerHTML}</div><div class="windowFooter"><div class="windowButtons">${buttonsHTML}</div></div></div>";
752
753 if (windowObj.buttons) {
754 for (const butt of windowObj.buttons) {
755 if (butt.f) {
756 window.querySelector('.windowButtons .winButton' + butt.number).addEventListener('click', e => {
757 butt.f.apply(task, [window]);
758 });
759 }
760 }
761 }
762
763 //Установка заголовка
764 window.titleBlock = window.querySelector('.blockTitle');
765 window.setTitle = function (title) {
766 window.titleBlock.innerHTML = title;
767 window.classList.add('hasTitle');
768 };
769 window.clearTitle = function () {
770 window.titleBlock.innerHTML = '';
771 window.classList.remove('hasTitle');
772 };
773 if (windowObj.title) {
774 window.setTitle(windowObj.title);
775 }
776
777 //Установка содержимого
778 window.content = window.querySelector('.windowContent');
779 window.setContent = function (content) {
780 window.content.innerHTML = task.compileHandlebars(content);
781 };
782 window.clearContent = function () {
783 window.content.innerHTML = '';
784 };
785 if (windowObj.content && !window.isTemporary) {
786 window.setContent(windowObj.content);
787 }
788
789 this.windows[windowName] = window;
790
791 if (window.onRender) window.onRender();
792 }
793 },
794
795 //Обработчик, выполняющийся при любой смене ШВшной солюшки.
796 onSolutionChange: function (solution) {
797 if (!this.rendered) {return;}
798 if (!solution) {
799 const eAndS = this.gatherErrorsAndSolution('Без вывода ошибок');
800 solution = eAndS.solution;
801
802 //Проверки
803 if (window.solutionChecks) {solutionChecks.apply(this, [solution]);}
804 }
805
806 //Если в солюшке есть кастомный объект - сохраняем его, чтобы не затёрся
807 if (this.theOneSolution && this.theOneSolution.__theOneCustomStorage) {
808 solution.__theOneCustomStorage = this.theOneSolution.__theOneCustomStorage;
809 }
810
811 this.theOneSolution = solution;
812 this.theOneSolutionUpdated = true;
813 },
814
815 //Очищает значения(солюшку) на всех контролах в указанном месте
816 clearSolutionHere: function (where) {
817 const root = this.getDOMElement(), task = this;
818 Array.from(where.querySelectorAll('.btn.active')).forEach(function (btn) {
819 if (btn.closest('.alien')) {
820 return;
821 }
822 task.buttonSmashed(btn, true);
823 task.checksAfterValidation(btn);
824 });
825 Array.from(where.querySelectorAll('input, textarea')).slice().reverse().forEach(function (input) {
826 if (input.closest('.alien')) {
827 return;
828 }
829 input.value = '';
830 input.classList.remove('validationRejected', 'validationPassed');
831 task.checksAfterValidation(input);
832 });
833 this.onSolutionChange();
834 },
835
836 //Загружает в таск переданную солюшку
837 loadSolution: function (solution, where) {
838 const task = this, root = where ? where : this.getDOMElement();
839
840 if (!solution) {
841 solution = this.theOneSolution;
842 }
843
844 //Обнуляем все контролы
845 this.clearSolutionHere(root);
846
847 function recursiveApplying(solution, where) {
848 for (let eName in solution) {
849 let e, ea;
850 if (where.classList.contains('groupContainer') && where.classList.contains('infinite')) {
851 e = where.querySelector(':scope>.group:last-child .fieldName_' + eName + '');
852 } else {
853 ea = where.querySelectorAll('.fieldName_' + eName);
854 e = ea[ea.length - 1];
855 }
856 if (!e) {
857 continue;
858 }
859
860 //Разбираемся, что это за элемент и в зависимости от этого применяем значение
861 if (e.classList.contains('fieldType_textarea')) {
862 let textarea = e.querySelector('textarea');
863 if (!textarea) {
864 continue;
865 }
866 textarea.value = solution[eName];
867 task.validateInput(textarea);
868 } else if (e.classList.contains('radioGroup') || e.classList.contains('select')) {
869 let btn = e.querySelector('.fieldName_' + solution[eName] + ' .btn');
870 if (!btn) {
871 continue;
872 }
873 if (!isActive(btn)) task.buttonSmashed(btn);
874 } else if (e.classList.contains('checkboxGroup')) {
875 Array.from(solution[eName]).forEach(buttName => {
876 let btn = e.querySelector('.fieldName_' + buttName + ' .btn');
877 if (!btn) {
878 return;
879 }
880 if (!isActive(btn)) task.buttonSmashed(btn);
881 });
882 } else if (e.classList.contains('buttDiv') && solution[eName]) {
883 let btn = e.querySelector('.btn');
884 if (!btn) {
885 continue;
886 }
887 if (!isActive(btn)) task.buttonSmashed(btn);
888 } else if (e.classList.contains('fieldType_date') || e.classList.contains('fieldType_text') || e.classList.contains('fieldType_email') || e.classList.contains('fieldType_link') || e.classList.contains('fieldType_integer') || e.classList.contains('fieldType_float')) {
889 let input = e.querySelector('input');
890 if (!input) {
891 continue;
892 }
893 input.value = solution[eName];
894 task.validateInput(input);
895 } else if (e.classList.contains('fileField') && e.classList.contains('virtual') && solution[eName]) {
896 const
897 fuFiles = task.getDOMElement().querySelector('.__fileUploader .field_file__files'),
898 fileDeleteButtons = fuFiles.querySelectorAll('.field_file__files__file .file__delete');
899
900 e.clear();
901
902 if (fileDeleteButtons && fileDeleteButtons.length > 0) {
903 task._fileUploader.uploadedFilesDivs = fuFiles.querySelectorAll('.field_file__files__file');
904 //Отрисовываем в контроле загруженные в нём файлы, которые есть в солюшке
905 for (const fileDeleteButt of fileDeleteButtons) {
906 const attachmentId = fileDeleteButt.dataset.attachmentId;
907 const item = task.getStorageItem('__uploadingFiles')[attachmentId];
908 if (item) {
909 item.fileName = fileDeleteButt.parentElement.querySelector('.file__name').innerHTML;
910 if (item.fieldName === e.dataset.name &&
911 solution[e.dataset.name] &&
912 Array.isArray(solution[e.dataset.name]) &&
913 (solution[e.dataset.name].includes(item.fileName) || solution[e.dataset.name].includes(attachmentId))
914 ) {
915 item.fileField = e;
916 item.fileLine = e.addFileLine(item.fileName, attachmentId);
917 }
918 }
919 }
920 }
921 e.validate({}, true);
922 } else if (e.classList.contains('groupContainer')) {
923 Array.from(solution[eName]).forEach(s => {
924 recursiveApplying(s, e);
925 task.checksAfterValidation(e);
926 });
927 }
928 }
929 }
930
931 //Применяем солюшку
932 recursiveApplying(solution, root);
933
934 this.onSolutionChange(solution);
935 },
936
937 //Инициализирует неинициализированные поля. Вешает на отрисованные ШВшные элементы обработчики событий. Вам сюда лезть не надо, на свои элементы вешайте всё, что хотите, в onRender.
938 initFieldsHere: function (whereToInit) {
939 const task = this, root = this.getDOMElement();
940 if (!whereToInit) {
941 return;
942 }
943 //Пробегаемся по всем инпутам и вешаем на них валидэйшн
944 Array.from(whereToInit.querySelectorAll('input:not(.initialized), textarea:not(.initialized)')).forEach(function (input) {
945 if (input.parentElement.classList.contains('searchField') || input.classList.contains('comment')) {
946 return;
947 }
948 input.addEventListener('input', function () {
949 if (this.closest('.alien')) {
950 return;
951 }
952
953 //Если у нас ридонли и инпут попытались поменять - возвращаем как было
954 if (task.rendered && (task.isReadOnly || this.interfaceBlocked) && !this.closest('.notReadOnly')) {
955 if (this.dataset.name && task.theOneSolution[this.dataset.name]) {
956 if (this.value !== task.theOneSolution[this.dataset.name]) {
957 this.value = task.theOneSolution[this.dataset.name];
958 }
959 } else {
960 if (this.value !== '') {
961 this.value = '';
962 }
963 }
964 }
965
966 if (task.validateInput(this)) {
967 task.lastChangedElement = this;
968 }
969 task.checksAfterValidation(this);
970 task.onSolutionChange();
971
972 //Проверки для обработчиков событий
973 const eHandlers = task.elementEventHandlers[input.dataset.name];
974 if (eHandlers) {
975 if (eHandlers.onInput) {
976 eHandlers.onInput.apply(task, [this]);
977 }
978 }
979 });
980 if (window.actionLogger) {
981 input.addEventListener('change', function () {
982 //Логируем нажатие
983 const charLimit = window.actionLoggerInputAndTextareaCharLimit || 201;
984 task.logAction('inputChange', {
985 buttonName: this.dataset.name,
986 value: this.value.length < charLimit ? this.value : "[>${charLimit} символов]"
987 });
988 });
989 }
990 task.validateInput(input);
991 input.classList.add('initialized');
992 });
993
994 //Теперь кнопки
995 Array.from(whereToInit.querySelectorAll('.btn:not(.initialized)')).forEach(function (btn) {
996 btn.addEventListener('click', function (event) {
997 if (btn.dataset.select && isActive(btn) && btn.closest('.cantUnselect')) {
998 return;
999 }
1000 if (btn.closest('.readOnly') && task.rendered === true) {return;}
1001
1002 task.buttonSmashed(this);
1003
1004 if (!this.closest('.actionButtonGroup') && !this.closest('.linkButtonGroup')) {
1005 task.checksAfterValidation(this);
1006 task.onSolutionChange();
1007 }
1008 //Проверки для обработчиков событий
1009 const eventHandlersName = btn.closest('.element') && btn.closest('.element').dataset.name ? "${btn.closest('.element').dataset.name}___${btn.dataset.name}" : btn.dataset.name;
1010 const buttHandlers = task.elementEventHandlers[eventHandlersName];
1011 if (buttHandlers) {
1012 if (buttHandlers.onClick) {
1013 buttHandlers.onClick.apply(task, [btn]);
1014 }
1015 }
1016
1017 //Логируем нажатие
1018 const additional = {};
1019 let action = 'buttonClick';
1020 if (this.dataset.name && this.dataset.name !== "undefined") {
1021 additional.buttonName = this.dataset.name;
1022 }
1023 if (!this.closest('.actionButtonGroup') && !this.closest('.linkButtonGroup')) {
1024 additional.isActive = isActive(this);
1025 }
1026 const e = this.closest('.element');
1027 if (e && e.dataset.name) {
1028 if (!e.classList.contains('spoiler')) {
1029 additional.element = e.dataset.name;
1030 } else {
1031 additional.spoiler = e.dataset.name;
1032 if (isActive(this)) {
1033 action = 'spoilerOpen';
1034 } else {
1035 action = 'spoilerClose';
1036 }
1037 }
1038 }
1039 if (this.closest('.groupContainer') && this.closest('.groupContainer').dataset.name) {
1040 additional.group = this.closest('.groupContainer').dataset.name;
1041 }
1042 task.logAction(action, additional);
1043
1044 });
1045 btn.addEventListener('mousedown', function () {
1046 btn.classList.add('underPressure');
1047 });
1048 btn.addEventListener('mousedown', function () {
1049 btn.classList.add('touchstart');
1050 });
1051 btn.addEventListener('mouseup', function () {
1052 btn.classList.remove('underPressure');
1053 });
1054 btn.addEventListener('mouseleave', function () {
1055 btn.classList.remove('underPressure');
1056 });
1057 btn.addEventListener('touchend', function () {
1058 btn.classList.remove('underPressure');
1059 });
1060 btn.classList.add('initialized');
1061 });
1062
1063 //Селекты
1064 Array.from(whereToInit.querySelectorAll('.select.element:not(.initialized)')).forEach(function (select) {
1065 select.querySelector('.selectButtons .arrow').addEventListener('click', e => {
1066 select.classList.toggle('opened');
1067 });
1068 select.querySelector('.selectDisplay').addEventListener('click', e => {
1069 select.classList.toggle('opened');
1070 });
1071 select.onKey = function (code, e) {
1072 if (code === "Space" && (!document.activeElement || (document.activeElement && !document.activeElement.closest('.noHotkeys')))) {
1073 e.preventDefault();
1074 select.querySelector('.selectButtons .arrow').click();
1075 }
1076 };
1077 select.classList.add('initialized');
1078 });
1079
1080 //Спойлеры
1081 Array.from(whereToInit.querySelectorAll('.spoiler.element:not(.initialized)')).forEach(function (spoiler) {
1082 spoiler.querySelector('.spoilerHead .btn').addEventListener('click', e => {
1083 spoiler.classList.toggle('opened');
1084 if (spoiler.classList.contains('opened')) {
1085 //task.getAssignment().getTaskSuite().maybeScrollToThisElement(spoiler, true);
1086 }
1087 });
1088 spoiler.onKey = function (code, e) {
1089 if (code === "Space" && (!document.activeElement || (document.activeElement && !document.activeElement.closest('.noHotkeys')))) {
1090 e.preventDefault();
1091 spoiler.querySelector('.spoilerHead .btn').click();
1092 }
1093 };
1094 spoiler.classList.add('initialized');
1095 });
1096
1097 //Поля загрузки файлов
1098 const initFilesFields = () => {
1099 Array.from(whereToInit.querySelectorAll('.fileField.element:not(.initialized)')).forEach(function (fileField) {
1100 task.initFileField(fileField);
1101 });
1102 };
1103 if (this.rendered) {
1104 initFilesFields();
1105 } else {
1106 document.ready.then(() => {
1107 initFilesFields();
1108 }).catch(err => console.error(err));
1109 }
1110
1111 //Инициализируем все ШВшные просмотрщики изображений
1112 Array.from(whereToInit.querySelectorAll('.element.imageField:not(.initialized)')).forEach(field => {
1113 initImageField(field);
1114 field.classList.add('initialized');
1115 });
1116
1117 //Вставляем СВГ-иконки
1118 this.insertIcons();
1119
1120 //Биндим действия на все helpContainer'ы
1121 for (const q of root.querySelectorAll('.helpContainer:not(.initialized)')) {
1122 initHelpContainer(q);
1123 }
1124
1125
1126 //Перестраиваем все горячие клавиши
1127 if (this.rendered) {
1128 this.getAssignment().getTaskSuite().initTheOneHotkeys();
1129 }
1130
1131 //Пробегаемся по всем groupContainer'ам и рисуем в них первый набор полей (может и последний, в зависимости от параметра infinite)
1132 Array.from(whereToInit.querySelectorAll('.groupContainer:not(.alien)')).forEach(function (gc) {
1133 if (gc.closest('.alien') || (gc.parentElement.classList.contains('group') && gc.parentElement !== whereToInit)) {
1134 return;
1135 }
1136 task.addGroup(gc);
1137 });
1138 },
1139
1140 //После валидации иногда нужно проводить разные проверки, все они находятся тут.
1141 //Если вы будете ковырять и модифицировать стандартные "бесконечные" группы полей - возможно, надо будет тут чёнить дописать.
1142 checksAfterValidation: function (validatedElement) {
1143 //Проверки для элементов внутри групп
1144 const gc = validatedElement.closest('.groupContainer'), props = null;
1145 if (gc) {
1146 //Валидируем группы в ГруппКонтейнере
1147 Array.from(gc.querySelectorAll(':scope>.group')).forEach(function (group) {
1148 //.hasSomething
1149 if (group.querySelector('.validationPassed') || group.querySelector('.validationRejected') || group.querySelector('.active')) {
1150 group.classList.add('hasSomething');
1151 } else {
1152 group.classList.remove('hasSomething');
1153 }
1154 //.correct
1155 if (!group.querySelector('.validationRejected') && !group.querySelector(':scope>.element .clear:not(.skipMe)')) {
1156 group.classList.add('correct');
1157 } else {
1158 group.classList.remove('correct');
1159 }
1160 });
1161 //Если валидированный элемент принадлежит к группе с бесконечными элементами - надо, мб, сгенерировать новых или, наоборот, удалить лишние
1162 if (isGroupInfinite(gc)) {
1163 //Добавление новых, если все группы в контейнере корректны
1164 if (!gc.querySelector(':scope>.group:not(.correct)') && (!gc.dataset.limit || (gc.dataset.limit && gc.querySelectorAll(':scope>.group').length < parseInt(gc.dataset.limit)))) {
1165 this.addGroup(gc);
1166 }
1167 //Удаление пустых
1168 const emptyGroups = gc.querySelectorAll(':scope>.group:not(.hasSomething)');
1169 let startFrom = 1;
1170 if (gc.querySelectorAll(':scope>.group').length > 1 && gc.querySelector(':scope>.group.hasSomething:not(.correct)')) {
1171 startFrom = 0;
1172 }
1173 if (emptyGroups && emptyGroups.length > startFrom) {
1174 for (let i = startFrom; i < emptyGroups.length; i++) {
1175 gc.removeChild(emptyGroups[i]);
1176 }
1177 }
1178 }
1179 }
1180 },
1181
1182 //Выполняется для каждой активированной ШВшной кнопки, будь то свитчБаттон, радиобаттон или чекбокс - без разницы.
1183 //Если вам нужно что-то делать после нажатия на кнопки (пусть даже некоторые) - внизу функции есть отличное место для кастомного кода. Хотя пишите куда хотите))))
1184 buttonSmashed: function (btn, isTemporary) {
1185 if (this.rendered &&
1186 (
1187 this.interfaceBlocked || (this.isReadOnly && !btn.closest('.notReadOnly')) ||
1188 btn.closest('.disabled')
1189 )
1190 ) {
1191 return;
1192 }
1193 const root = this.getDOMElement(), task = this;
1194 if (!btn.dataset.type || window.buttonsDisabled) {
1195 return;
1196 }
1197 this.hideTaskError();
1198 this.turnOffErrorClassOnParentElementTagBlock(btn);
1199 let props = null;
1200 if (btn.dataset.props) {
1201 props = fieldsPropsArray[btn.dataset.props];
1202 }
1203
1204 //Производит всякие проверки в зависимости от свойств этот кнопки
1205 function propsChecks() {
1206 //Создадим массив выключенных в данный момент групп, чтобы не включить ничего лишнего
1207 let excludedArr = [];
1208 if (props.includes || props.excludes) {
1209 Array.from(root.querySelectorAll('.btn.active.excludesSomething')).forEach(btn => {
1210 let props;
1211 if (btn.dataset.props) {
1212 props = fieldsPropsArray[btn.dataset.props];
1213 } else {
1214 return;
1215 }
1216 if (props.excludes) {
1217 props.excludes.forEach(groupName => {
1218 excludedArr.push(groupName);
1219 });
1220 }
1221 });
1222 }
1223
1224 //Если эта кнопка должна при включении что-то исключать или включать: делаем это
1225 if (props.excludes) {
1226 Array.from(props.excludes).forEach(function (excludedClass) {
1227 const excludedDivs = root.querySelectorAll(':not(.includes).' + excludedClass);
1228 Array.from(excludedDivs).forEach(function (excludedDiv) {
1229 if (isActive(btn)) {
1230 excludedDiv.classList.add('excluded');
1231 //И снимаем там все галки
1232 Array.from(excludedDiv.querySelectorAll('.btn.active')).forEach(function (btn) {
1233 btn.click();
1234 });
1235 } else {
1236 if (!excludedArr.includes(excludedClass) && !(excludedDiv.dataset.excluded && excludedDiv.dataset.excluded === 'excluded')) {
1237 excludedDiv.classList.remove('excluded');
1238 //Обновлянем все просмотрщики с канвасами
1239 for (const c of excludedDiv.querySelectorAll('.imageField .imageBlock canvas')) {
1240 if (c.initCanvas) {
1241 c.initCanvas();
1242 }
1243 }
1244 }
1245 }
1246 });
1247 });
1248 }
1249 if (props.includes) {
1250 Array.from(props.includes).forEach(function (includedClass) {
1251 //Сначала проверим, может ничего и не надо делать
1252 if (!isActive(btn) && root.querySelector('.active.includes.' + includedClass)) {
1253 return;
1254 }
1255
1256 const includedDivs = root.querySelectorAll(':not(.includes).' + includedClass);
1257 Array.from(includedDivs).forEach(function (includedDiv) {
1258 if (!isActive(btn)) {
1259 includedDiv.classList.add('excluded');
1260 //И снимаем там все галки
1261 if (includedDiv.querySelector('.btn.active')) {
1262 Array.from(includedDiv.querySelectorAll('.btn.active')).forEach(function (btn) {
1263 btn.click();
1264 });
1265 }
1266 } else {
1267 if (!excludedArr.includes(includedClass)) {
1268 includedDiv.classList.remove('excluded');
1269 //Обновлянем все просмотрщики с канвасами
1270 for (const c of includedDiv.querySelectorAll('.imageField .imageBlock canvas')) {
1271 if (c.initCanvas) {
1272 c.initCanvas();
1273 }
1274 }
1275 }
1276 }
1277 });
1278 });
1279 }
1280
1281 //valuesToWrite - массив обьектов, описывающий какое значение записать в определённое свойство объекта с указанным селектором
1282 if (props.valuesToWrite) {
1283 if (!Array.isArray(props.valuesToWrite)) {
1284 ce('valuesToWrite_is_not_an_array', btn.parentElement.className);
1285 } else {
1286 props.valuesToWrite.forEach(function (valObj) {
1287 let ok = true;
1288 if (!valObj.selector) {
1289 ce('valuesToWrite_has_no_selector', btn.parentElement.className);
1290 ok = false;
1291 }
1292 if (!valObj.property) {
1293 ce('valuesToWrite_has_no_property', btn.parentElement.className);
1294 ok = false;
1295 }
1296 if (!valObj.value && valObj.value !== '') {
1297 ce('valuesToWrite_has_no_value', btn.parentElement.className);
1298 ok = false;
1299 }
1300 if (ok === false) {
1301 return;
1302 }
1303 let target = root.querySelector(valObj.selector);
1304 if (!target) {
1305 ce('valuesToWrite_target_not_found', 'Класс кнопки: ' + btn.parentElement.className + ', селектор: ' + valObj.selector);
1306 ok = false;
1307 } else {
1308 target[valObj.property] = valObj.value;
1309 }
1310 });
1311 }
1312 }
1313 }
1314
1315 switch (btn.dataset.type) {
1316 case 'switchButton':
1317 //Если эта кнопка - часть группы радиобаттонов и её включили, то снимаем остальные радиобаттоны в этой группе
1318 if ((btn.dataset.radiogroup || btn.dataset.select) && !isActive(btn)) {
1319 Array.from(btn.parentElement.parentElement.querySelectorAll('.btn.active')).forEach(function (b) {
1320 task.buttonSmashed(b);
1321 });
1322 }
1323 //Перекрашиваем кнопку
1324 invertButton(btn);
1325
1326 if (props) {
1327 propsChecks();
1328 }
1329 break;
1330 case 'linkButton':
1331 if (props) {
1332 propsChecks();
1333 }
1334 btn.classList.add('clicked');
1335 break;
1336 }
1337
1338 //Если это кнопка селекта
1339 if (btn.dataset.select) {
1340 const
1341 select = btn.closest('.select'),
1342 selectText = select.querySelector('.selectText');
1343 if (isActive(btn)) {
1344 selectText.innerHTML = btn.querySelector('a').innerHTML;
1345 select.dataset.selection = btn.dataset.name;
1346 } else {
1347 selectText.innerHTML = '';
1348 select.dataset.selection = '';
1349 }
1350 select.classList.remove('opened');
1351 }
1352
1353 //Проверки для обработчиков событий
1354 const eventHandlersName = btn.closest('.element') && btn.closest('.element').dataset.name ? "${btn.closest('.element').dataset.name}___${btn.dataset.name}" : btn.dataset.name;
1355 const buttHandlers = this.elementEventHandlers[eventHandlersName];
1356 if (!isTemporary && buttHandlers && btn.lastActivationState !== isActive(btn)) {
1357 if (buttHandlers.onActivation && isActive(btn)) {
1358 buttHandlers.onActivation.apply(this, [btn]);
1359 }
1360 if (buttHandlers.onDeactivation && !isActive(btn)) {
1361 buttHandlers.onDeactivation.apply(this, [btn]);
1362 }
1363 }
1364
1365 if (!isTemporary) btn.lastActivationState = isActive(btn);
1366
1367 this.lastChangedElement = btn;
1368 },
1369
1370 //Служебная функция, добавляет группу полей в группКонтейнер. Здесь реально не на что смотреть, проходите.
1371 addGroup: function (gc /* groupContainer */) {
1372 const group = document.createElement('div'), task = this;
1373 group.className = isGroupInfinite(gc) ? 'group infiniteChild' : 'group';
1374 group.innerHTML = fieldsPropsArray[gc.dataset.codepropindex];
1375 gc.appendChild(group);
1376 this.initFieldsHere(group);
1377 //На случай вложенных групп
1378 Array.from(gc.querySelectorAll('.groupContainer')).forEach(function (gcInner) {
1379 const c = gc.closest('.groupContainer');
1380 if (c && c === gc) {return;}
1381 task.addGroup(gcInner);
1382 });
1383
1384 },
1385
1386 //Функция валидации всех ШВшных инпутов и текстарей. Если вам нужна какая-то непредусмотренная валидация - смело дополняйте в отведённом месте.
1387 validateInput: function (input) {
1388 let checksPassed;
1389 if (input.closest('.alien')) {
1390 input.classList.add('skipMe');
1391 return;
1392 }
1393 const task = this, root = this.getDOMElement(),
1394 parent = input.closest('.group') || input.closest('.answerBlock');
1395 let isValidationPassed = false, //validationPassed - поле прошло валидацию, validationRejected - не прошло.
1396 props = null;
1397 if (input.dataset.props) {
1398 props = fieldsPropsArray[input.dataset.props];
1399 }
1400 if (!input.value) {
1401 input.classList.add('clear');
1402 input.classList.remove('validationPassed');
1403 input.classList.remove('validationRejected');
1404 if (isOptional(props)) {
1405 input.classList.add('skipMe');
1406 }
1407 //this.turnOffErrorClassOnParentElementTagBlock(input);
1408 return false;
1409 } else {
1410 input.classList.remove('skipMe');
1411 input.classList.remove('clear');
1412 }
1413
1414 //Определяемся с валидацией - либо используем описанную у самого поля (если есть), либо по общим правилам
1415 const eHandlers = task.elementEventHandlers[input.dataset.name];
1416 if (eHandlers && eHandlers.validation) {
1417 isValidationPassed = eHandlers.validation.apply(this, [input]);
1418 } else {
1419 switch (input.dataset.type) {
1420 case 'date':
1421 if (isDate(input.value, (props && props.format ? props.format.toUpperCase() : 'DD-MM-YYYY'))) {
1422 isValidationPassed = true;
1423 }
1424 if (props) {
1425 if (isValidationPassed && (props.aheadOf || props.behindOf)) {
1426 checksPassed = true;
1427 let aheadOfWhat = null, behindOfWhat = null;
1428
1429 if (props.aheadOf) {
1430 aheadOfWhat = parent.querySelector('.fieldName_' + props.aheadOf + ' .validationPassed');
1431 }
1432 if (props.behindOf) {
1433 behindOfWhat = parent.querySelector('.fieldName_' + props.behindOf + ' .validationPassed');
1434 }
1435
1436 if (props.aheadOf && aheadOfWhat) {
1437 //Надо сравнить обе даты, приведя каждую из них к удобному виду
1438 checksPassed = isFirstDateAheadOfSecond(input.value, props.format ? props.format.toUpperCase() : 'DD-MM-YYYY', aheadOfWhat.value, aheadOfWhat.dataset.props ? (fieldsPropsArray[aheadOfWhat.dataset.props].format ? fieldsPropsArray[aheadOfWhat.dataset.props].format.toUpperCase() : 'DD-MM-YYYY') : 'DD-MM-YYYY');
1439 }
1440 if (checksPassed && props.behindOf && behindOfWhat) {
1441 //Надо сравнить обе даты, приведя каждую из них к удобному виду
1442 checksPassed = isFirstDateAheadOfSecond(behindOfWhat.value, behindOfWhat.dataset.props ? (fieldsPropsArray[behindOfWhat.dataset.props].format ? fieldsPropsArray[behindOfWhat.dataset.props].format.toUpperCase() : 'DD-MM-YYYY') : 'DD-MM-YYYY', input.value, props.format ? props.format.toUpperCase() : 'DD-MM-YYYY');
1443 }
1444 isValidationPassed = checksPassed;
1445 }
1446 }
1447 break;
1448 case 'text':
1449 case 'textarea':
1450 checksPassed = true;
1451 if (props) {
1452 if (props.flags) {
1453 if (props.flags.includes('cyrillic') && !/^[\u0430-\u044f\s-"]*$/i.test(input.value)) {
1454 checksPassed = false;
1455 }
1456 if (props.flags.includes('firstCapital') && !/^([\u0410-\u042F][\u0430-\u044f]+\s?)*$/.test(input.value)) {
1457 checksPassed = false;
1458 }
1459 }
1460 if (props.min && input.value.length < props.min) {
1461 checksPassed = false;
1462 }
1463 if (props.max && input.value.length > props.max) {
1464 checksPassed = false;
1465 }
1466 }
1467 isValidationPassed = checksPassed;
1468 break;
1469 case 'link':
1470 if (isLink(input.value)) {
1471 isValidationPassed = true;
1472 }
1473 if (props) { //В свойствах поля задан конкретный список сервисов, поэтому валидируем ссылку на принадлежность к каждому из них.
1474 if (isValidationPassed && props.services) {
1475 let pass;
1476 if (typeof props.services === 'string') {
1477 props.services = [props.services];
1478 }
1479 for (const service of props.services) {
1480 if (isServiceLink(input.value, service.toLowerCase())) {
1481 pass = true;
1482 break;
1483 }
1484 }
1485 if (!pass) {
1486 isValidationPassed = false;
1487 }
1488 }
1489 if (isValidationPassed && props.domains) {
1490 let pass;
1491 if (typeof props.domains === 'string') {
1492 props.domains = [props.domains];
1493 }
1494 for (const domain of Array.from(props.domains)) {
1495 if (new URL(input.value).host.includes(domain)) {
1496 pass = true;
1497 break;
1498 }
1499 }
1500 if (!pass) {
1501 isValidationPassed = false;
1502 }
1503 }
1504 }
1505 break;
1506 case 'email':
1507 if (isEmail(input.value)) {
1508 isValidationPassed = true;
1509 }
1510 break;
1511 case 'integer':
1512 checksPassed = isInteger(input.value);
1513 if (props) {
1514 if (checksPassed && props.min && parseInt(input.value) < props.min) {
1515 checksPassed = false;
1516 }
1517 if (checksPassed && props.max && parseInt(input.value) > props.max) {
1518 checksPassed = false;
1519 }
1520 }
1521 isValidationPassed = checksPassed;
1522 break;
1523 case 'float':
1524 checksPassed = false;
1525
1526 checksPassed = isInteger(input.value) || isFloat(input.value);
1527 if (props) {
1528 if (checksPassed && props.min && parseFloat(input.value) < props.min) {
1529 checksPassed = false;
1530 }
1531 if (checksPassed && props.max && parseFloat(input.value) > props.max) {
1532 checksPassed = false;
1533 }
1534 }
1535 isValidationPassed = checksPassed;
1536 break;
1537 }
1538 //Проверка на соответствие глобальным лимитам
1539 if (isValidationPassed) {
1540 if ((input.dataset.type === 'textarea' && window.textareaCharLimit && input.value.length > window.textareaCharLimit) || ((input.dataset.type !== 'textarea' && window.inputCharLimit && input.value.length > window.inputCharLimit))) {
1541 isValidationPassed = false;
1542 }
1543 }
1544 }
1545
1546 //Проверка на уникальность при наличии нужного флага
1547 if (input.value && isValidationPassed && props && props.flags && props.flags.includes('unique')) {
1548 let allInputsVals = [];
1549 Array.from(root.querySelectorAll('input')).forEach(i => {
1550 if (!i.value || i === input) {
1551 return;
1552 }
1553 let iValue;
1554 if (i.dataset.type === 'link') {
1555 iValue = skipProtocolParametersAndOtherStuff(i.value);
1556 } else {
1557 iValue = i.value;
1558 }
1559 allInputsVals.push(iValue);
1560 });
1561 let iValue;
1562 if (input.dataset.type === 'link') {
1563 iValue = skipProtocolParametersAndOtherStuff(input.value);
1564 } else {
1565 iValue = input.value;
1566 }
1567 if (allInputsVals.includes(iValue) || (props && props.uniqueAdditional && Array.isArray(props.uniqueAdditional) && props.uniqueAdditional.includes(iValue))) {
1568 isValidationPassed = false;
1569 }
1570 }
1571
1572 //Место для кастомного кода
1573
1574 if (!isValidationPassed) {
1575 input.classList.remove('validationPassed');
1576 input.classList.add('validationRejected');
1577 } else {
1578 input.classList.add('validationPassed');
1579 input.classList.remove('validationRejected');
1580 this.turnOffErrorClassOnParentElementTagBlock(input);
1581 }
1582
1583 return true;
1584 },
1585
1586 //Я не знаю, кто написал эту функцию. Когда я стал лид-асессором, она уже была написана кем-то (Денисом Раскостовым?) и все её использовали в своих шаблонах. А мы чё, хуже?)))
1587 addError: function (message, field, errors) {
1588 errors || (errors = {
1589 task_id: this.getOptions().task.id,
1590 errors: {}
1591 });
1592 errors.errors[field] = {
1593 message: message
1594 };
1595
1596 return errors;
1597 },
1598
1599 //Аналог вышеописанной функции, но вешает на конкретные DOM-элементы ШВшную плашку с ошибкой (а не на Хэндлбарсный контрол). Используйте на здоровье при кастомной валидации!
1600 addErrorToField: function (field, msg, errors) {
1601 const root = this.getDOMElement(), task = this;
1602
1603 const wrappedMSG = "<div class="errorMsgBlockInner">${msg}</div>";
1604
1605 if (!field.classList.contains('element')) {
1606 field = field.closest('.element');
1607 }
1608
1609 if (!field.dataset.errorMsgNumber) {
1610 field.dataset.errorMsgNumber = this.errorMSGs.length;
1611 let newMSG = document.createElement('div');
1612 newMSG.className = 'errorMsgBlock';
1613 newMSG.innerHTML = wrappedMSG;
1614 this.errorMSGs.push(field.insertBefore(newMSG, field.firstElementChild));
1615 } else {
1616 this.errorMSGs[field.dataset.errorMsgNumber].innerHTML = wrappedMSG;
1617 }
1618 this.turnOnErrorClassOnParentElementTagBlock(field);
1619 return this.addError(msg, 'Fallout 76', errors);
1620 },
1621
1622 //Всё понятно из названия, а если нет - то из содержания уж точно. Вам, наверное, не понадобится.
1623 turnOnErrorClassOnParentElementTagBlock: function (someElement) {
1624 let parentContainer = someElement.closest('.element');
1625 if (parentContainer) {
1626 parentContainer.classList.add('withSomeError');
1627 }
1628
1629 makeSureThisDivFitsOnScreen(parentContainer.querySelector('.errorMsgBlock'));
1630
1631 },
1632
1633 //Всё понятно из названия, а если нет - то из содержания уж точно. Может вам понадобиться, если где-то надо спрятать плашку с ошибкой, выведенной addErrorToField().
1634 turnOffErrorClassOnParentElementTagBlock: function (someElement) {
1635 let parentContainer = someElement.closest('.element');
1636 if (parentContainer) {
1637 parentContainer.classList.remove('withSomeError');
1638 }
1639 },
1640
1641 //Функция отвечает за обработку параметра convert в спекМаппере
1642 convertSpecField: function (res, convert, func) {
1643 let task = this;
1644 if (typeof (convert) == "string") {
1645 convert = [convert];
1646 }
1647 convert.forEach(conv => {
1648 switch (conv) {
1649 case "strToBool":
1650 res = !!(res === true || (res && ['true', 'yes', 'yep', 'sure', 'да'].includes(res.toLowerCase())));
1651 break;
1652 case "invert":
1653 res = !res;
1654 break;
1655 case "infiniteToStringArray":
1656 if (res) {
1657 let result = [];
1658 for (const line of res) {
1659 for (const key in line) {
1660 result.push(line[key]);
1661 }
1662 }
1663 res = result;
1664 } else {
1665 res = null;
1666 }
1667 break;
1668 case "function":
1669 if (!func) {
1670 ce('specmapper_conversion_no_function', 'type: ' + conv);
1671 } else {
1672 res = func.apply(task, [res]);
1673 }
1674 break;
1675 default:
1676 ce('specmapper_conversion_type_not_found', 'type: ' + conv);
1677 }
1678 });
1679 return res;
1680 },
1681
1682 //Тот самый спекМаппер, о котором вам рассказывали на экскурсии по Звёздному Крейсеру "Галактика". И нет, я не сайлон. Наверное...
1683 mapSpecField: function (mapperObj, solution) {
1684 let root = this.getDOMElement(), task = this, res;
1685
1686 if (!mapperObj.function && mapperObj.f) {
1687 mapperObj.function = mapperObj.f;
1688 }
1689 if (!mapperObj.convert && mapperObj.function) {
1690 mapperObj.convert = ['function'];
1691 }
1692
1693 gettingRes: {
1694
1695 if (mapperObj.from) {
1696 if (Array.isArray(mapperObj.from)) {
1697 for (const name of mapperObj.from) {
1698 if (this.output[name] || this.output[name] === false) {
1699 res = this.output[name];
1700 break gettingRes;
1701 }
1702 }
1703 } else {
1704 if (this.output[mapperObj.from] || this.output[mapperObj.from] === false) {
1705 res = this.output[mapperObj.from];
1706 break gettingRes;
1707 }
1708 }
1709 }
1710
1711 if (mapperObj.fromT) {
1712 if (Array.isArray(mapperObj.fromT)) {
1713 for (const name of mapperObj.fromT) {
1714 if (solution[name] || solution[name] === false) {
1715 res = solution[name];
1716 break gettingRes;
1717 }
1718 }
1719 } else {
1720 if (solution[mapperObj.fromT] || solution[mapperObj.fromT] === false) {
1721 res = solution[mapperObj.fromT];
1722 break gettingRes;
1723 }
1724 }
1725 }
1726
1727 if (!mapperObj.from && !mapperObj.fromT) {
1728 res = this.output;
1729 }
1730
1731 }
1732
1733 if (mapperObj.convert) {
1734 res = this.convertSpecField(res, mapperObj.convert, mapperObj.function);
1735 }
1736
1737 return res;
1738 },
1739
1740 //Серьёзно, вам нужен комментарий для этой функции?
1741 validate: function (solution) {
1742 const root = this.getDOMElement(), task = this, input = this.getTask().input_values;
1743 if (printSolution) {
1744 console.log('Таск:', root.dataset.number);
1745 console.log('Солюшка до валидации:', Object.assign({}, solution.output_values));
1746 }
1747 let errors, output;
1748 //Валидация и сбор солюшки по ШВшным элементам
1749 if (!this.dontGatherSolution) {
1750 let eAndS = this.gatherErrorsAndSolution();
1751 errors = eAndS.errors;
1752 output = eAndS.solution;
1753 } else {
1754 output = this.theOneSolution;
1755 }
1756 this.output = output;
1757
1758
1759 //СпекМаппер
1760 let specFields = 0, res = false;
1761 if (useSpecMapper) {
1762 res = {};
1763
1764 //Если в HTML есть скрытое поле для загрузки файлов - его надо обязательно смаппить в выходное поле files
1765 if (root.querySelector('.element.__fileUploader')) {
1766 specMapper.files = {fromT: "files"};
1767 }
1768
1769 for (let outField in specMapper) {
1770 specFields++;
1771 const value = this.mapSpecField(specMapper[outField], solution.output_values);
1772 if (value !== undefined) {
1773 res[outField] = value;
1774 }
1775 }
1776
1777 if (usingOneRawData) {
1778 const tord = $.extend(true, {}, this.theOneSolution);
1779 tord.__theOneCustomStorage.__actionLog = []; //Экономим трафик и место
1780 tord.__theOneCustomStorage.__timingsStorage = {}; //Оно там не нужно
1781 delete tord.undefined; //Были прецеденты)))
1782 res.__theOneRawData = JSON.stringify(tord);
1783 if (printSolution) {
1784 console.log('__theOneRawData:', JSON.parse(res['__theOneRawData']));
1785 }
1786 }
1787 }
1788
1789 //Валидация
1790 if (window.validation) {errors = validation.apply(this, [res, output, solution, errors, root]);}
1791
1792 //Завершения валидэйта
1793 solution.output_values = res ? res : {"result": output};
1794 errors = errors || TolokaHandlebarsTask.prototype.validate.apply(this, solution);
1795 if (errors && window.noValidationErrors) {
1796 window.noValidationErrors = false;
1797 }
1798 if (printSolution) {
1799 console.log('Солюшка ШВ:', $.extend(true, {}, this.output), ' Полная: ', $.extend(true, {}, this.theOneSolution));
1800 console.log('Итоговая солюшка:', solution.output_values);
1801 console.log('Ошибки:', errors);
1802
1803 }
1804 return errors;
1805 },
1806
1807 //Функция для ШВшной валидации и сбора солюшки во всём таске. Если beQuiet - не ругается на ошибки и не собирает их, а просто собирает валидную солюшку.
1808 //whereToGather - либо селектор, в котором всё это делать, либо элемент с уникальным в рамках таска className
1809 gatherErrorsAndSolution: function (beQuiet, whereToGather) {
1810 let root = this.getDOMElement(), task = this, selectorPrefix,
1811 output = {}, errors = null,
1812 validationGroupIndex = -1;
1813
1814 selectorPrefix = whereToGather ? (
1815 typeof whereToGather == 'string' ?
1816 whereToGather :
1817 (() => {
1818 let s = '';
1819 for (const item of whereToGather.classList) {
1820 s += '.' + item;
1821 }
1822 return s + ' ';
1823 })()
1824 ) : '.answerBlock ';
1825
1826 //Добавляем и валидируем все обычные поля
1827 const valResult = this.validateAndGrabFieldsInHere(selectorPrefix, errors, beQuiet, true);
1828 output = valResult.value;
1829 errors = valResult.errors;
1830
1831 /* Теперь у нам предстоит 2 прохода по бесконечным группам - сначала мы все нумеруем, а потом собираем солюшку. */
1832
1833 //Индексируем группы для оптимизации дальнейшего алгоритма и отладки
1834 Array.from(root.querySelectorAll(selectorPrefix + '.groupContainer:not(.excluded)')).forEach(function (gc) {
1835
1836 const groups = gc.querySelectorAll(':scope>.group');
1837
1838 for (let i = 0; i < groups.length; i++) {
1839 validationGroupIndex++;
1840 if (groups[i].dataset.validationGroupIndex) {
1841 groups[i].classList.remove('validationGroupIndex' + groups[i].dataset.validationGroupIndex);
1842 }
1843 groups[i].dataset.validationGroupIndex = validationGroupIndex;
1844 groups[i].classList.add('validationGroupIndex' + validationGroupIndex);
1845 groups[i].dataset.numberInContainer = i;
1846 }
1847
1848 });
1849
1850 //Добавляем и валидируем группКонтейнеры погруппно каждый
1851 Array.from(root.querySelectorAll(selectorPrefix + '.groupContainer:not(.excluded)')).forEach(function (gc) {
1852 const groups = gc.querySelectorAll(':scope>.group');
1853
1854 for (let i = 0; i < groups.length; i++) {
1855 //Если это какая-то "левая", неШВшная группа
1856 if (groups[i].parentElement !== gc) {
1857 continue;
1858 }
1859
1860 //Проверки для "бесконечных" групп
1861 if (isGroupInfinite(groups[i].parentElement)) {
1862 //Если группа пустая, но при этом есть другие - пропускаем (для "бесконечных" групп)
1863 groups[i].classList.remove('infiniteSkipped');
1864 if (i > 0 && !groups[i].classList.contains('hasSomething') && !groups[i].querySelector('.group.hasSomething')) {
1865 groups[i].classList.add('infiniteSkipped');
1866 continue;
1867 } else if (i === 0 && !groups[i].classList.contains('hasSomething') && groups[i].closest('.group.infiniteSkipped')) {
1868 continue;
1869 }
1870 }
1871
1872 const valResult = task.validateAndGrabFieldsInHere(selectorPrefix + ".groupContainer .group.validationGroupIndex${groups[i].dataset.validationGroupIndex}>div:not(.groupContainer)", errors, beQuiet);
1873 //Если valResult непустой - записываем его в выходную спеку
1874 if (valResult.isNotEmpty) {
1875 if (isGroupInfinite(gc)) {
1876 const gcChain = [{name: gc.dataset.name, number: i}];
1877 let parentInfiniteChildGroup = gc.closest('.group.infiniteChild');
1878 if (parentInfiniteChildGroup) { //Если эта бесконечная группа вложена как минимум в какую-то другую
1879
1880 //Идём вверх по родителям и создаём последовательность имён и номеров групп, чтобы получить как-бы адрес текущей группы в солюшке
1881 do {
1882 gcChain.push({
1883 name: parentInfiniteChildGroup.parentElement.dataset.name,
1884 number: parseInt(parentInfiniteChildGroup.dataset.numberInContainer)
1885 });
1886 parentInfiniteChildGroup = parentInfiniteChildGroup.parentElement.closest('.group.infiniteChild');
1887 } while (parentInfiniteChildGroup);
1888
1889 //Теперь перемещаем курсор снаружи вглубь солюшки по составленной цепочке попутно создавая нужные ключи, если их нет
1890 let cursor = output;
1891 for (let n = gcChain.length-1; n >= 0; n--) {
1892 const
1893 gcName = gcChain[n].name,
1894 gcNumber = gcChain[n].number;
1895 if (!cursor[gcName]) {
1896 cursor[gcName] = [];
1897 }
1898 if (!cursor[gcName][gcNumber]) {
1899 cursor[gcName][gcNumber] = {};
1900 }
1901 cursor = cursor[gcName][gcNumber];
1902 }
1903
1904 //На данный момент наш курсор находится в финальной точке и указывает на конечный объект в солюшке для текущей группы.
1905 //Остаётся лишь заполнить его собранной солюшкой
1906 for (let val in valResult.value) {
1907 cursor[val] = valResult.value[val];
1908 }
1909
1910 } else { //Если текущая бесконечная группа у нас не содержится внутри другой бесконечной группы - всё намного проще
1911 if (!output[gc.dataset.name]) {
1912 output[gc.dataset.name] = [];
1913 }
1914 if (!output[gc.dataset.name][i]) {
1915 output[gc.dataset.name][i] = {};
1916 }
1917 for (let val in valResult.value) {
1918 output[gc.dataset.name][i][val] = valResult.value[val];
1919 }
1920 }
1921 } else {
1922 for (let val in valResult.value) {
1923 output[val + (i > 0 ? i : '')] = valResult.value[val];
1924 }
1925 }
1926 }
1927 errors = valResult.errors;
1928 }
1929 });
1930
1931 //Проверяем заполненность всех radioGroup
1932 Array.from(root.querySelectorAll(selectorPrefix + '.radioGroup:not(.optional)')).forEach(function (radioGroup) {
1933 if (!beQuiet && !radioGroup.querySelector('.btn.active') && !radioGroup.closest('.excluded') && !radioGroup.closest('.window:not(.opened)')) {
1934 const group = radioGroup.closest('.group');
1935 if (group) {
1936 const gc = group.parentElement;
1937 if (gc && group && group.closest('.infiniteSkipped')) {
1938 return;
1939 }
1940 if (gc && group && isGroupInfinite(gc) && !group.classList.contains('hasSomething') && gc.querySelector('.group.hasSomething')) {
1941 return;
1942 }
1943 }
1944 errors = task.addErrorToField(radioGroup, 'Пожалуйста, выберите соответствующую кнопку!', errors);
1945 }
1946 });
1947
1948 //Проверяем заполненность всех checkboxGroup
1949 Array.from(root.querySelectorAll(selectorPrefix + '.checkboxGroup:not(.optional)')).forEach(function (checkboxGroup) {
1950 if (!beQuiet && !checkboxGroup.querySelector('.btn.active') && !checkboxGroup.closest('.excluded') && !checkboxGroup.closest('.window:not(.opened)')) {
1951 const group = checkboxGroup.closest('.group');
1952 if (group) {
1953 const gc = group.parentElement;
1954 if (gc && group && group.closest('.infiniteSkipped')) {
1955 return;
1956 }
1957 if (gc && group && isGroupInfinite(gc) && !group.classList.contains('hasSomething') && gc.querySelector('.group.hasSomething')) {
1958 return;
1959 }
1960 }
1961 errors = task.addErrorToField(checkboxGroup, 'Пожалуйста, выберите соответствующую кнопку!', errors);
1962 }
1963 });
1964
1965 //Проверяем заполненность всех селектов
1966 Array.from(root.querySelectorAll(selectorPrefix + '.select:not(.optional)')).forEach(function (select) {
1967 if (!beQuiet && !select.dataset.selection && !select.closest('.excluded') && !select.closest('.window:not(.opened)')) {
1968 const group = select.closest('.group');
1969 if (group) {
1970 const gc = group.parentElement;
1971 if (gc && group && group.closest('.infiniteSkipped')) {
1972 return;
1973 }
1974 if (gc && group && isGroupInfinite(gc) && !group.classList.contains('hasSomething') && gc.querySelector('.group.hasSomething')) {
1975 return;
1976 }
1977 }
1978 errors = task.addErrorToField(select, 'Пожалуйста, выберите элемент выпадающего списка!', errors);
1979 }
1980 });
1981
1982 return {"errors": errors, "solution": output};
1983 },
1984
1985 //Производит ШВшную валидацию и сбор ШВшной солюшки (далее по тексту - просто "Валидация") в указанном элементе.
1986 validateAndGrabFieldsInHere: function (selector, errors, beQuiet, notInGroup) {
1987 const task = this, root = this.getDOMElement(), result = {"value": {}, "errors": errors, "isNotEmpty": false};
1988 //Добавляем все инпуты
1989 Array.from(root.querySelectorAll(selector + ' input, ' + selector + ' textarea')).forEach(function (field) {
1990 if (notInGroup && field.closest('.groupContainer')) {
1991 return result;
1992 }
1993 if (field.closest('.excluded') || field.closest('.alien') || field.closest('.window:not(.opened)')) {
1994 return result;
1995 } //Дополнительная проверка для инпутов вложенных полей
1996 task.validateInput(field);
1997 if (field.classList.contains('skipMe')) {
1998 return;
1999 }
2000 if (!field.classList.contains('validationPassed')) {
2001 if (!beQuiet) {
2002 result.errors = task.addErrorToField(field, 'Проверьте, пожалуйста, корректность заполнения этого поля.', result.errors);
2003 }
2004 } else {
2005 result.value[field.dataset.solutionname || field.dataset.name] = field.value;
2006 result.isNotEmpty = true;
2007 }
2008 });
2009
2010 //Добавляем все нажатые кнопки
2011 Array.from(root.querySelectorAll(selector + ' .btn.active')).forEach(function (btn) {
2012 if (notInGroup && btn.closest('.groupContainer')) {
2013 return result;
2014 }
2015 if (btn.closest('.excluded') || btn.closest('.alien') || btn.closest('.window:not(.opened)')) {
2016 return result;
2017 }
2018 let groupSolutionName = btn.dataset.groupsolutionname || btn.dataset.radiogroup || btn.dataset.select || btn.dataset.checkboxgroup;
2019 if (btn.dataset.radiogroup) { //Эта кнопка - часть массива радиобаттонов
2020 result.value[groupSolutionName] = btn.dataset.name;
2021 } else if (btn.dataset.select) { //Эта кнопка - часть селекта
2022 result.value[groupSolutionName] = btn.dataset.name;
2023 } else if (btn.dataset.checkboxgroup) { //Эта кнопка - часть массива чекбоксов (от обычных чекбоксов они отличаются сбором данных в массив)
2024 if (!result.value[groupSolutionName]) {
2025 result.value[groupSolutionName] = [];
2026 }
2027 result.value[groupSolutionName].push(btn.dataset.name);
2028 } else {
2029 result.value[btn.dataset.name] = true;
2030 }
2031 result.isNotEmpty = true;
2032 });
2033
2034 //Добавляем выключенные кнопки
2035 if (typeof buttonsCanBeFalse === "undefined" || buttonsCanBeFalse) {
2036 Array.from(root.querySelectorAll(selector + '.buttonGroup .btn:not(.active)')).forEach(function (btn) {
2037 if (notInGroup && btn.closest('.groupContainer')) {
2038 return result;
2039 }
2040 if (btn.closest('.excluded') || btn.closest('.alien') || btn.closest('.window:not(.opened)')) {
2041 return result;
2042 }
2043 let groupSolutionName = btn.dataset.groupsolutionname || btn.dataset.radiogroup || btn.dataset.select || btn.dataset.checkboxgroup;
2044 result.value[btn.dataset.name] = false;
2045 result.isNotEmpty = true;
2046 });
2047 }
2048
2049 //Валидируем ШВшные загрузчики файлов
2050 for (const fileField of root.querySelectorAll(selector + ' .element.fileField')) {
2051 if (notInGroup && fileField.closest('.groupContainer')) {
2052 continue;
2053 }
2054 if (fileField.closest('.excluded') || fileField.closest('.alien') || fileField.closest('.window:not(.opened)')) {
2055 continue;
2056 }
2057
2058 //Валидация
2059 result.errors = fileField.validate(result.errors, beQuiet);
2060
2061 //Сбор солюшки
2062 if (fileField.classList.contains('validationPassed')) {
2063 const files = [];
2064 const fileLines = fileField.querySelectorAll('.fcFileLine');
2065 for (const line of fileLines) {
2066 if (line.dataset.attachmentId) {
2067 files.push(line.dataset.attachmentId);
2068 }
2069 }
2070 if (files.length) {result.value[fileField.dataset.name] = files;}
2071 }
2072
2073 }
2074 return result;
2075 },
2076
2077 //Инициализирует поле для загрузки файла
2078 initFileField: function(field){
2079 const task = this, root = this.getDOMElement(),
2080 fContainer = field.querySelector('.fContainer'),
2081 fTitle = field.querySelector('.stepTitle'),
2082 fileUploader = root.querySelector('.__fileUploader'),
2083 fieldName = field.dataset.name;
2084
2085 if (!fileUploader) {
2086 ce('Вы добавили в спеку поле типа file, оно отрендерилось, но вы не добавили для него стандартный Толоковский загручик файлов. Читайте доку.', field);
2087 return;
2088 }
2089
2090 const fuFiles = fileUploader.querySelector('.field_file__files');
2091 if (!fuFiles) {
2092 ce('Очень странно, у загрузчика файлов не найден div .field_file__files...', fileUploader);
2093 return;
2094 }
2095
2096 let props;
2097 if (field.dataset.props) {
2098 props = fieldsPropsArray[field.dataset.props];
2099 }
2100
2101 //Раз у нас в шаблоне есть скрытый загрузчик файлов - надо повесить на него обсервер, если ещё не повесили
2102 if (!this._fileUploader) {this._fileUploader = {};}
2103 if (!this._fileUploader.observer) {
2104 this._fileUploader.uploadedFilesDivs = [];
2105 this._fileUploader.observer = new MutationObserver((mutations) => {
2106 const filesDivs = fuFiles.querySelectorAll('.field_file__files__file');
2107 const uploadingFiles = {};
2108
2109 //Пробегаемся по всем дивам с файлами и загоняем имена в объект
2110 let nameOfLastFile;
2111 for (const fDiv of filesDivs) {
2112 const fileName = fDiv.querySelector('.file__name').innerHTML;
2113 const fileDeleteButt = fDiv.querySelector('.file__delete');
2114 const attachmentId = fileDeleteButt.dataset.attachmentId;
2115 if (attachmentId) {
2116 uploadingFiles[attachmentId] = {
2117 fileName: fileName
2118 };
2119 if (this.getStorageItem('__uploadingFiles')[attachmentId]) {
2120 if (this.getStorageItem('__uploadingFiles')[attachmentId].fileLine) {
2121 uploadingFiles[attachmentId].fileLine = this.getStorageItem('__uploadingFiles')[attachmentId].fileLine;
2122 }
2123 if (this.getStorageItem('__uploadingFiles')[attachmentId].fileField) {
2124 uploadingFiles[attachmentId].fileField = this.getStorageItem('__uploadingFiles')[attachmentId].fileField;
2125 }
2126 if (this.getStorageItem('__uploadingFiles')[attachmentId].fieldName) {
2127 uploadingFiles[attachmentId].fieldName = this.getStorageItem('__uploadingFiles')[attachmentId].fieldName;
2128 }
2129 }
2130 }
2131 nameOfLastFile = fileName;
2132 }
2133
2134 //Если добавили файл
2135 if (filesDivs.length > this._fileUploader.uploadedFilesDivs.length) {
2136 if (this._fileUploader.someFileUploading) {
2137 this._fileUploader.someFileUploading(nameOfLastFile);
2138 }
2139 } else
2140 //Если удалили файл
2141 if (filesDivs.length < this._fileUploader.uploadedFilesDivs.length) {
2142 //Сначала надо определить, какой именно
2143 let deletedFileName;
2144 for (const f in uploadingFiles) {
2145 let weGotIt;
2146 for (const _f in this.getStorageItem('__uploadingFiles')) {
2147 if (f === _f) {
2148 weGotIt = true;}
2149 }
2150 if (!weGotIt) {
2151 if (this._fileUploader.someFileRemoved) {
2152 this._fileUploader.someFileRemoved(f);
2153 }
2154 break;
2155 }
2156 }
2157 } else
2158
2159 //Если количество не изменилось - значит какой-то файл закончил загружаться
2160 {
2161 if (this._fileUploader.uploadComplete) {
2162 this._fileUploader.uploadComplete(filesDivs);
2163 }
2164 }
2165
2166 this.setStorageItem('__uploadingFiles', uploadingFiles);
2167 this._fileUploader.uploadedFilesDivs = filesDivs;
2168
2169 });
2170 this._fileUploader.observer.observe(fuFiles, { attributes: false, childList: true, characterData: false });
2171 }
2172
2173 //Добавляет к полю загрузчик файла
2174 field.addUploader = () => {
2175
2176 function openFileDialog() {
2177 fileUploader.querySelector('.field_file__upload').click();
2178 }
2179
2180 const waitTillTheFileFinishUploading = () => {
2181 return new Promise(((resolve, reject) => {
2182 this._fileUploader.uploadComplete = (filesDivs) => {
2183 resolve(filesDivs);
2184 }
2185 }));
2186 };
2187
2188 const fuButtons = {
2189 type: "actionButtonGroup",
2190 flags: ["unfocusable"],
2191 buttons: [
2192 {
2193 iconClass: "uploadButtIcon",
2194 caption: "Загрузить",
2195 onClick() {
2196 if (this.isReadOnly || this.interfaceBlocked) {return;}
2197 if (this._fileUploader.someFileUploadingNow) {return;}
2198 openFileDialog();
2199 //Переопределяем функцию, которая будет вызвана после появления нового файла в загрузчике
2200 this._fileUploader.someFileUploading = (fileName) => {
2201 if (//Проверка расширения файла
2202 (props && props.extensions &&
2203 !(new RegExp("\\.(${
2204 (()=>{
2205 let s='';
2206 for(const e of props.extensions) {
2207 s+='|'+e;
2208 }
2209 return s.substr(1);
2210 })()
2211 })$", 'ig').test(fileName))
2212 ) ||
2213 //Проверка имени файла
2214 (() => {
2215 const uf = this.getStorageItem('__uploadingFiles');
2216 for (const attachmentId in uf) {
2217 if (uf[attachmentId].fileName === fileName) {
2218 this.windows._fileWithSameNameAlreadyUploadedMessageWindow.open();
2219 return true;
2220 }
2221 }
2222 })()
2223 ) {
2224 //Отменяем загрузку файла
2225 const thisFileDeleteButt = fileUploader.querySelector('.field_file__files__file_processing .file__delete');
2226 thisFileDeleteButt.click();
2227 return;
2228 }
2229
2230 this._fileUploader.someFileUploadingNow = true;
2231 const fileLine = this._fileUploader.uploadingFileLine = field.addFileLine(fileName);
2232
2233 waitTillTheFileFinishUploading().then((filesDivs) => {
2234 this._fileUploader.uploadingFileLine.uploadingComplete(filesDivs);
2235 });
2236 }
2237 }
2238 }
2239 ]
2240 };
2241 task.getJSON([fuButtons]);
2242 const fUploader = document.createElement('div');
2243 fUploader.className = "fUploader";
2244 fUploader.innerHTML = "
2245 <div class="fuButtons">${handleFieldAndGetHTML(fuButtons)}</div>
2246 ";
2247
2248 fTitle.appendChild(fUploader);
2249 task.initFieldsHere(fUploader);
2250 };
2251
2252 field.addUploader();
2253
2254 field.addFileLine = (fileName, attachmentId) => {
2255
2256 const flButtons = {
2257 type: "actionButtonGroup",
2258 flags: ["unfocusable"],
2259 buttons: [
2260 {
2261 iconClass: "thrashButtIcon",
2262 caption: "Удалить",
2263 onClick() {
2264 if (this.isReadOnly || this.interfaceBlocked) {return;}
2265 fileLine.remove();
2266 }
2267 }
2268 ]
2269 };
2270
2271 task.getJSON([flButtons]);
2272 const fileLine = document.createElement('div');
2273
2274 fileLine.remove = () => {
2275 //Перебираем все файлы в загрузчке и удаляем нужный
2276 for (const fDiv of this._fileUploader.uploadedFilesDivs) {
2277 const thisFileName = fDiv.querySelector('.file__name').innerHTML;
2278 const thisFileDeleteButt = fDiv.querySelector('.file__delete');
2279 const thisFileAttachmentId = thisFileDeleteButt.dataset.attachmentId;
2280 if (thisFileName === fileName && (!thisFileAttachmentId || attachmentId === thisFileAttachmentId)) {
2281 thisFileDeleteButt.click();
2282 break;
2283 }
2284 }
2285
2286 fileObj.fileLine.classList.remove('uploading');
2287 fContainer.classList.remove('somethingIsUploading');
2288 this._fileUploader.someFileUploadingNow = false;
2289
2290 //Удаляем строчку ШВшного загрузчика с данным файлом
2291 fContainer.removeChild(fileObj.fileLine);
2292
2293 this.turnOffErrorClassOnParentElementTagBlock(field);
2294 field.validate({}, true);
2295
2296 };
2297
2298 fileLine.className = "fcFileLine";
2299 if (!attachmentId) {
2300 fileLine.className += " uploading";
2301 }
2302 fileLine.innerHTML = "
2303 <div class="fcFileButtons">${handleFieldAndGetHTML(flButtons)}</div>
2304 <div class="fcFileName">${fileName}</div>
2305 ";
2306
2307 const fileObj = {
2308 fileName: fileName,
2309 fileLine: fileLine,
2310 fileField: field,
2311 fieldName: fieldName
2312 };
2313
2314 fContainer.appendChild(fileLine);
2315 task.initFieldsHere(fileLine);
2316
2317 if (!attachmentId) {
2318 fileLine.uploadingComplete = (filesDivs) => {
2319 fileLine.classList.remove('uploading');
2320 fContainer.classList.remove('somethingIsUploading');
2321 this._fileUploader.someFileUploadingNow = false;
2322
2323 //Получаем и прописываем attachmentId загруженного файла
2324 if (filesDivs) {
2325 for (const fDiv of filesDivs) {
2326 const fName = fDiv.querySelector('.file__name').innerHTML;
2327 if (fName !== fileName) {
2328 continue;
2329 }
2330 const fileDeleteButt = fDiv.querySelector('.file__delete');
2331 attachmentId = fileDeleteButt.dataset.attachmentId;
2332 break;
2333 }
2334
2335 fileLine.dataset.attachmentId = attachmentId;
2336 this.getStorageItem('__uploadingFiles')[attachmentId] = fileObj;
2337
2338 this.turnOffErrorClassOnParentElementTagBlock(field);
2339
2340 field.validate({}, true);
2341
2342 this.checksAfterValidation(field);
2343 this.onSolutionChange();
2344 }
2345 };
2346 } else {
2347 fileLine.dataset.attachmentId = attachmentId;
2348 }
2349
2350 fContainer.classList.add('somethingIsUploading');
2351
2352 return fileLine;
2353 };
2354
2355 field.clear = () => {
2356 fContainer.innerHTML = '';
2357 };
2358
2359 field.validate = (errors, beQuiet) => {
2360
2361 const fileLines = field.querySelectorAll('.fcFileLine');
2362 const minFiles = props && props.min ? parseInt(props.min) : false;
2363 const maxFiles = props && props.max ? parseInt(props.max) : false;
2364
2365 //Валидация
2366 let validationPassed = true;
2367 if (!beQuiet && !fileLines.length && !field.classList.contains('optional')) {
2368 if (!minFiles && !maxFiles) {
2369 validationPassed = false;
2370 errors = task.addErrorToField(field, 'Загрузите, пожалуйста, хотя бы один файл!', errors);
2371 } else {
2372 validationPassed = false;
2373 errors = task.addErrorToField(field, "Загрузите, пожалуйста, файлы! (${minFiles ? 'от ' + minFiles : ''}${maxFiles ? (minFiles ? ' ' : '') + 'до ' + maxFiles : ''})"
2374 , errors);
2375 }
2376 } else if (minFiles && !beQuiet && fileLines.length < minFiles) {
2377 validationPassed = false;
2378 errors = task.addErrorToField(field, "Минимальное количество файлов: ${minFiles}!", errors);
2379 } else if (maxFiles && !beQuiet && fileLines.length > maxFiles) {
2380 validationPassed = false;
2381 errors = task.addErrorToField(field, "Максимальное количество файлов: ${maxFiles}!", errors);
2382 }
2383 field.classList.remove('validationRejected');
2384 field.classList.remove('validationPassed');
2385 field.classList.remove('clear');
2386 field.classList.remove('skipMe');
2387 if (fileLines.length > 0) {
2388 if (validationPassed) {
2389 field.classList.add('validationPassed');
2390 } else {
2391 field.classList.add('validationRejected');
2392 }
2393 } else {
2394 field.classList.add('clear');
2395 if (field.classList.contains('optional')) {
2396 field.classList.add('skipMe');
2397 }
2398 }
2399
2400 return errors;
2401 };
2402
2403 //Если поле не виртуальное - отображаем все его загруженные файлы
2404 if (!field.classList.contains('virtual') && (!field.closest('.groupContainer ') || !isGroupInfinite(field.closest('.groupContainer')))) {
2405 const fileDeleteButtons = fuFiles.querySelectorAll('.field_file__files__file .file__delete');
2406 if (fileDeleteButtons && fileDeleteButtons.length > 0) {
2407 this._fileUploader.uploadedFilesDivs = fuFiles.querySelectorAll('.field_file__files__file');
2408 //Отрисовываем в контроле загруженные в нём файлы, которые есть в скрытом загрузчике
2409 for (const fileDeleteButt of fileDeleteButtons) {
2410 const attachmentId = fileDeleteButt.dataset.attachmentId;
2411 const item = this.getStorageItem('__uploadingFiles')[attachmentId];
2412 if (item) {
2413 item.fileName = fileDeleteButt.parentElement.querySelector('.file__name').innerHTML;
2414 if (item.fieldName === fieldName) {
2415 item.fileField = field;
2416 item.fileLine = field.addFileLine(item.fileName, attachmentId);
2417 }
2418 }
2419 }
2420 }
2421 field.validate({}, true);
2422 }
2423
2424 field.validate({}, true);
2425 field.classList.add('initialized');
2426 },
2427
2428 //Очищает список загруженных файлов (не удаляя их) у ШВшного поля загрузки файлов
2429 clearFileField: function(name) {
2430 const field = this.getDOMElement().querySelector('.element.fileField.fieldName_'+name);
2431 if (!field) {ce('Не найдено поле загрузки файлов для его очистки', name); return;}
2432 field.clear();
2433 },
2434
2435 //Открывает окно жалобщика
2436 submitComplaint: function () {
2437 if (!useComplainter) {
2438 return;
2439 }
2440 const assignmentInfo = this.getAssignment().getOptions().assignment;
2441 window.Complaints.default.open({
2442 inputValues: this.getTask().input_values,
2443 assignment: {
2444 assignmentId: assignmentInfo.id,
2445 poolId: assignmentInfo.poolId,
2446 taskSuiteId: assignmentInfo.taskSuiteId
2447 }
2448 }, this.getAssignment().getId());
2449 },
2450
2451 //Заново рендерит указанный контрол с новой спекой и сохранением солюшки
2452 replaceField: function (name, json) {
2453 const root = this.getDOMElement(), field = root.querySelector('.element.' + name);
2454 if (!field) {
2455 return;
2456 }
2457
2458 this.loadSolution(this.theOneSolution);
2459 },
2460
2461 //Записывает в солюшку все поля переданного объекта
2462 writeObjectToSolution: function (object, postfix, solution) {
2463 if (!solution) {solution = this.theOneSolution || {};}
2464 if (!object) {return solution;}
2465
2466 for (const item in object) {
2467 solution[item + postfix] = object[item];
2468 }
2469
2470 return solution;
2471 },
2472
2473 //Заново отрисовывает указанный контрол с сохранением солюшки
2474 redrawField: function (name, group) {
2475 const root = this.getDOMElement(),
2476 field = root.querySelector('.element.' + name);
2477 if (!field) {
2478 return;
2479 }
2480 const fieldParent = field.parentElement;
2481 const fieldWasFocused = field.classList.contains('focused');
2482
2483 const specField = this.getSpecField(name, group);
2484 if (!specField) {
2485 return;
2486 }
2487
2488 field.outerHTML = handleFieldAndGetHTML.apply(this, [specField]);
2489 this.initFieldsHere(fieldParent);
2490 this.loadSolution(this.theOneSolution, fieldParent);
2491
2492 if (fieldWasFocused) {
2493 const newField = root.querySelector('.element.' + name);
2494 newField.click();
2495 }
2496 },
2497
2498 redrawFields: function (fieldsName) {
2499 const root = this.getDOMElement(),
2500 container = root.querySelector('.theOneFieldsContainer.fieldsName_' + fieldsName),
2501 spec = this.theOneSpec.spec[fieldsName];
2502 if (!container) {
2503 ce("В redrawFields передано несуществующее имя theOneFieldsContainer'a", fieldsName);
2504 return;
2505 }
2506 if (!spec) {
2507 ce("В redrawFields передано несуществующее имя спеки theOneSpec", fieldsName);
2508 return;
2509 }
2510 for (let container of root.querySelectorAll('.theOneFieldsContainer.fieldsName_' + fieldsName)) {
2511 const p = container.parentElement;
2512 container.outerHTML = getFieldsHTML.apply(this, [fieldsName]);
2513 container = p.querySelector('.theOneFieldsContainer');
2514 this.initFieldsHere(container);
2515 this.loadSolution(this.theOneSolution, container);
2516 }
2517 },
2518
2519 //Возвращает ссылку на конкретное поле в спеке Всевластия
2520 getSpecField: function (name, group) {
2521 if (!this.theOneSpec.specByName[name]) {
2522 return
2523 }
2524 const field = group ?
2525 this.theOneSpec.specByName[name].getElementByName(group).groupFields.getElementByName(name) :
2526 this.theOneSpec.specByName[name].getElementByName(name);
2527 return field;
2528 },
2529
2530 //Устанавливает/заменяет массив полей в спеке Всевластия
2531 setSpecFields: function(fieldsName, value) {
2532 this.theOneSpec.spec[fieldsName] = this.getJSON(value);
2533 this.getTask().input_values[fieldsName] = this.theOneSpec.spec[fieldsName];
2534 },
2535
2536 includeGroup: function (name) {
2537 const root = this.getDOMElement(),
2538 groupDiv = root.querySelector('.groupContainer.fieldName_' + name);
2539 if (!groupDiv) {
2540 return;
2541 }
2542
2543 groupDiv.classList.remove('excluded');
2544 },
2545
2546 excludeGroup: function (name) {
2547 const root = this.getDOMElement(),
2548 groupDiv = root.querySelector('.groupContainer.fieldName_' + name);
2549 if (!groupDiv) {
2550 return;
2551 }
2552
2553 groupDiv.classList.add('excluded');
2554 },
2555
2556 focusNextTask() {
2557 this.getAssignment().getTaskSuite().focusNextTask();
2558 },
2559
2560 focusPreviousTask() {
2561 this.getAssignment().getTaskSuite().focusPreviousTask();
2562 },
2563
2564 /* Работа с видами (view) */
2565
2566 //Прячет указанный view
2567 hideView: function (name) {
2568 const root = this.getDOMElement(),
2569 view = root.querySelector('.view.' + name);
2570 if (!view) {
2571 return;
2572 }
2573
2574 view.classList.add('hidden');
2575 },
2576
2577 //Показывает указанный view
2578 showView: function (name) {
2579 const root = this.getDOMElement(),
2580 view = root.querySelector('.view.' + name);
2581 if (!view) {
2582 return;
2583 }
2584
2585 view.classList.remove('hidden');
2586 //Обновлянем все просмотрщики с канвасами
2587 for (const c of view.querySelectorAll('.imageField .imageBlock canvas')) {
2588 if (c.initCanvas) {
2589 c.initCanvas();
2590 }
2591 }
2592 },
2593
2594 //Исключает из сбора солюшки указанный view
2595 excludeView: function (name) {
2596 const root = this.getDOMElement(),
2597 view = root.querySelector('.view.' + name);
2598 if (!view) {
2599 return;
2600 }
2601
2602 view.classList.add('excluded');
2603 },
2604
2605 //Включает в сбор солюшки указанный view
2606 includeView: function (name) {
2607 const root = this.getDOMElement(),
2608 view = root.querySelector('.view.' + name);
2609 if (!view) {
2610 return;
2611 }
2612
2613 view.classList.remove('excluded');
2614 },
2615
2616 //Прячет все view
2617 hideAllViews: function (className) {
2618 const root = this.getDOMElement();
2619 if (!className) {
2620 className = 'view';
2621 }
2622
2623 for (const view of root.querySelectorAll('.view.' + className)) {
2624 view.classList.add('hidden');
2625 }
2626 },
2627
2628 //Выключает сбор солюшки во всех view
2629 excludeAllViews: function (className) {
2630 const root = this.getDOMElement();
2631 if (!className) {
2632 className = 'view';
2633 }
2634
2635 for (const view of root.querySelectorAll('.view.' + className)) {
2636 view.classList.add('excluded');
2637 }
2638 },
2639
2640 //Показывает все view
2641 showAllViews: function (className) {
2642 const root = this.getDOMElement();
2643 if (!className) {
2644 className = 'view';
2645 }
2646
2647 for (const view of root.querySelectorAll('.view.' + className)) {
2648 view.classList.remove('hidden');
2649 //Обновлянем все просмотрщики с канвасами
2650 for (const c of view.querySelectorAll('.imageField .imageBlock canvas')) {
2651 if (c.initCanvas) {
2652 c.initCanvas();
2653 }
2654 }
2655 }
2656 },
2657
2658 //Включает сбор солюшки во всех view
2659 includeAllViews: function (className) {
2660 const root = this.getDOMElement();
2661 if (!className) {
2662 className = 'view';
2663 }
2664
2665 for (const view of root.querySelectorAll('.view.' + className)) {
2666 view.classList.remove('excluded');
2667 }
2668 },
2669
2670 //Показывает только указыннй view (остальные прячет)
2671 showOnlyThisView: function (name, className) {
2672 const root = this.getDOMElement();
2673
2674 this.hideAllViews(className);
2675 this.showView(name);
2676 },
2677
2678 //Включает сбор солюшки только в указанном view
2679 includeOnlyThisView: function (name, className) {
2680 const root = this.getDOMElement();
2681
2682 this.excludeAllViews(className);
2683 this.includeView(name);
2684 },
2685
2686 //Компилирует HTML+Handlebars в контексте текущего таска
2687 compileHandlebars: function (code, optionalContext) {
2688 const context = optionalContext || this.handlebarsContext || this.getTask().input_values;
2689 return Handlebars.compile(code)(context);
2690 },
2691
2692 //Сохраняет данные в ШВ-шное локальное хранилище
2693 setStorageItem: function (key, value) {
2694 this.theOneSolution.__theOneCustomStorage[key] = value;
2695 this.theOneSolutionUpdated = true;
2696 },
2697
2698 //Получает данные из ШВ-шного локального хранилища
2699 getStorageItem: function (key) {
2700 if (this.theOneSolution.__theOneCustomStorage && this.theOneSolution.__theOneCustomStorage[key]) {
2701 return this.theOneSolution.__theOneCustomStorage[key];
2702 }
2703 },
2704
2705 //Раньше в шаблоне использовался иконочный шрифт material icons. Функция ищет все старые и новые иконки в коде и меняет их на свгшки.
2706 insertIcons: function (where) {
2707 let root = where ? where : this.getDOMElement(), c = 0;
2708 Array.from(root.querySelectorAll('i.material-icons:not(.processed)')).forEach(i => {
2709 c++;
2710 i.dataset.icon = i.innerHTML;
2711 i.innerHTML = '';
2712 i.classList.add('ffIcon');
2713 });
2714 Array.from(root.querySelectorAll('.ffIcon:not(.processed)')).forEach(i => {
2715 let icon = svgIcons[i.dataset.icon];
2716 if (!icon) {
2717 return;
2718 }
2719
2720 let html = '<svg focusable="false" xmlns="http://www.w3.org/2000/svg" ' + icon.attributes + '>';
2721
2722 if (icon.translucentPath) {
2723 html += '<path fill="currentColor" d="' + icon.translucentPath + '" opacity="0.4"></path>';
2724 }
2725 if (icon.path) {
2726 html += '<path fill="currentColor" d="' + icon.path + '"></path>';
2727 }
2728
2729 i.innerHTML = html + '</svg>';
2730 i.classList.add('processed');
2731 });
2732 },
2733
2734 /* Логирование действий */
2735 getTimeStamp: function () {
2736 const date = new Date(), curTime = date.getTime(), ts = this.getStorageItem('__timingsStorage');
2737 let timestamp = ts.total + (curTime - ts.focused);
2738 return timestamp;
2739 },
2740
2741 //Обеспечивает задержку в логировании, потому что если кликнуть по элементу несфокусированного таска, то сначала сработает события клика по элементу, а только потом сфокусируется таск.
2742 logAction: function (action, additional) {
2743 if (!actionLogger || !this.rendered) {
2744 return;
2745 }
2746 setTimeout(() => {
2747 this.logActionNow(action, additional);
2748 }, 60);
2749 },
2750
2751 logActionNow: function (action, additional) {
2752 const logObj = {
2753 time: this.getTimeStamp(),
2754 action: action
2755 }, ts = this.getStorageItem('__timingsStorage');
2756 if (additional) {
2757 logObj.additional = additional;
2758 }
2759 this.getStorageItem('__actionLog').push(logObj);
2760 },
2761
2762 onFocus: function () {
2763 if (!actionLogger || !this.rendered) {
2764 return;
2765 }
2766 const date = new Date(), curTime = date.getTime(), ts = this.getStorageItem('__timingsStorage');
2767 if (!ts.started) {
2768 ts.started = curTime;
2769 }
2770 ts.focused = curTime;
2771 this.logActionNow('focus');
2772 },
2773
2774 onBlur: function () {
2775 if (!actionLogger || !this.rendered) {
2776 return;
2777 }
2778 const date = new Date(), curTime = date.getTime(), ts = this.getStorageItem('__timingsStorage');
2779 if (!ts.focused) {
2780 return;
2781 }
2782 ts.blurred = curTime;
2783 this.logActionNow('blur');
2784 ts.total += curTime - ts.focused;
2785 },
2786};
2787
2788
2789
2790
2791
2792
2793
2794/*
2795*
2796* Методы класса TaskSuite
2797*
2798*/
2799window.theOneTaskSuitePrototype = {
2800 onRender: function () {
2801 let root = this.getDOMElement(),
2802 _this = this;
2803 this.initSuite();
2804 document.ready.then(() => {
2805 //Кастомные штуки
2806
2807 }).catch(err => console.error(err));
2808
2809 //Кастомные штуки
2810
2811 },
2812
2813 //Инициализирует интерфейс Шаблона Всевластия
2814 initSuite: function () {
2815 let root = this.getDOMElement(),
2816 _this = this,
2817 tasks = root.querySelectorAll('.task'),
2818 isMobile = window.thisIsMobileDevice || window.screen.width <= 768;
2819 this.tasks = tasks;
2820 window.readyToKill = false;
2821 window.curTask = 4815162342;
2822
2823 cssNoClassSettings.forEach(settingName => {
2824 if (!eval(settingName)) {
2825 root.classList.add('no_' + settingName);
2826 }
2827 });
2828 cssClassSettings.forEach(settingName => {
2829 if (eval(settingName)) {
2830 root.classList.add(settingName);
2831 }
2832 });
2833
2834 //Установка флагов режима только для чтения и просмотра выполненных заданий
2835 if (this.getWorkspaceOptions().isReadOnly) {
2836 this.isReadOnly = true;
2837 }
2838 if (this.getWorkspaceOptions().isReviewMode) {
2839 this.isReviewMode = true;
2840 }
2841
2842
2843 //Инициализация поиска по глоссарию
2844 if (useGlossarySearch) {
2845 $.when(
2846 $.getScript(jQueryUILibSrc),
2847 $.getScript(glossarySearchLib),
2848 $.Deferred(function (deferred) {
2849 $(deferred.resolve);
2850 })
2851 ).done(function () {
2852 $('<link/>', {
2853 rel: 'stylesheet',
2854 type: 'text/css',
2855 href: jQueryUICssSrc
2856 }).appendTo('head');
2857
2858 for (const task of tasks) {
2859 $(task.querySelector('.btnGlossarySearch .glossarySearchForm')).glossSearch('add', {
2860 source: 'ajax_to_S3',
2861 url: glossarySearchSource
2862 });
2863 task.querySelector('.btnGlossarySearch a').addEventListener('click', function () {
2864 this.parentElement.classList.toggle('active');
2865 });
2866 }
2867 });
2868 }
2869
2870 //Инициализация Жалобщика
2871 if (useComplainter) {
2872 for (const task of tasks) {
2873 task.querySelector('.btnComplainter a').addEventListener('click', function () {
2874 _this.getTasks()[task.dataset.number].submitComplaint();
2875 });
2876 }
2877 }
2878
2879 //Формируем tabList у переключалки тасков
2880 //Формируем HTML для выпадашек - таскопереключалок
2881 let tabListHTML = '', i = 0;
2882 Array.from(tasks).forEach(task => {
2883 if (!task.querySelector('.tabList')) {
2884 return;
2885 } //Это чтобы таски не перепутались, если где-то отсутствует таблист
2886 i++;
2887 let name = task.querySelector('.taskName');
2888 name = name ? name.innerHTML : 'Задание ' + i;
2889 tabListHTML += ' <div class="tabListItem" data-number="' + (i - 1) + '">' +
2890 '<div class="tabListItemNumber">' + i + '.</div>' +
2891 '<div class="tabListItemName">' +
2892 name +
2893 '</div>' +
2894 '</div>';
2895 });
2896 //И прописываем его в переключалки тасков
2897 i = 0;
2898 Array.from(tasks).forEach(task => {
2899 let tabList = task.querySelector('.tabList');
2900 if (tabList) {
2901 tabList.innerHTML = '<div>' + tabListHTML + '</div>';
2902
2903 //Обрабатываем элементы выпадашки
2904 let j = 0;
2905 Array.from(tabList.querySelectorAll('.tabListItem')).forEach(tlItem => {
2906 let curItemNumber = j;
2907 if (i === j) {
2908 tlItem.classList.add('selected');
2909 } else
2910 tlItem.addEventListener('click', function (e) {
2911 e.preventDefault();
2912 e.stopPropagation();
2913 let n = parseInt(this.dataset.number);
2914
2915 if (!verticalSuite) {
2916 window.scrollingNow = true;
2917 root.style.left = -100 * n + 'vw';
2918 setTimeout(function () {
2919 _this.focusTask(n, false);
2920 }, 50);
2921 } else {
2922 window.scrollingNow = true;
2923 setTimeout(function () {
2924 _this.focusTask(n, true);
2925 $(root).animate({scrollTop: tasks[n].offsetTop}, 200);
2926 }, 50);
2927 }
2928
2929 });
2930 j++;
2931 });
2932 //После отрисовки страницы прописываем таблисту правильную высоту при ховере, чтобы анимация работала максимально красиво
2933 document.ready.then(() => {
2934 setTimeout(() => {
2935 document.styleSheets[0].insertRule('.tabScroller:hover .tabList {max-height: ' + tabList.offsetHeight + 'px;}');
2936 document.styleSheets[0].insertRule('.tabList {max-height: 0;}');
2937 }, 0);
2938 }).catch(err => console.error(err));
2939 i++;
2940 }
2941 });
2942
2943 //Переключение между тасками свайпом
2944 if (isMobile) {
2945 Array.from(tasks).forEach(task => {
2946 let swipe = {};
2947
2948 function mbTouchStart(e) {
2949 swipe.startX = e.touches[0].clientX;
2950 swipe.startY = e.touches[0].clientY;
2951 }
2952
2953 function mbTouchEnd(e) {
2954 swipe.endX = e.changedTouches[0].clientX;
2955 swipe.endY = e.changedTouches[0].clientY;
2956 if (!swipe.startX || !swipe.startY || !swipe.endX || !swipe.endY) {
2957 return
2958 }
2959 swipe.diffX = Math.abs(swipe.startX - swipe.endX);
2960 swipe.diffY = Math.abs(swipe.startY - swipe.endY);
2961 if (swipe.diffX > window.screen.width / 2.5 && swipe.diffX > swipe.diffY * 2.5) {
2962 //Надо бы свайпнуть
2963 if (swipe.startX < swipe.endX) {
2964 task.querySelector('.leftScroll').click();
2965 } else {
2966 task.querySelector('.rightScroll').click();
2967 }
2968 }
2969 }
2970
2971 task.addEventListener('touchstart', mbTouchStart, false);
2972 task.addEventListener('touchend', mbTouchEnd, false);
2973 });
2974 }
2975
2976 this.initTheOneHotkeys();
2977
2978 document.addEventListener('keydown', function (event) {
2979 if ((document.activeElement && document.activeElement.closest('.noHotkeys'))) {return;}
2980 if (window.onSuiteKeydown) {
2981 if (onSuiteKeydown.apply(_this, [event.code, root, event])) {return;}
2982 }
2983 if (_this.focusedElement && _this.focusedElement.onKey) {
2984 _this.focusedElement.onKey(event.code, event);
2985 }
2986 //Удерживание кнопки не ставит вердикт, а показывает подсказку (совместно с onKey и onkeyup)
2987 if (!event.repeat) {
2988 _this.keydownTime = Date.now();
2989 const key = event.key.toUpperCase();
2990 if (elementButtonsHotkeysOrderString.includes(key)) {
2991 _this.keyHoldTimeout = setTimeout(function () {
2992 if (_this.focusedElement && _this.focusedElement.buttons && _this.focusedElement.closest('.task_focused')) {
2993 const btn = _this.focusedElement.buttons[elementButtonsHotkeysOrderString.indexOf(key)];
2994 if (!btn) {
2995 return;
2996 }
2997 const popup = btn.parentElement.querySelector('.popUpTitle');
2998 if (!popup) {
2999 return;
3000 }
3001 _this.openedPopup = popup;
3002 popup.parentElement.classList.add('openedByKey');
3003 }
3004 }, timeToOpenHintByKey);
3005 }
3006 }
3007
3008 if (event.code === "Tab") {
3009 event.preventDefault();
3010 }
3011 if (!hotkeysEnabled) {
3012 return;
3013 }
3014 if (event.code === "Escape" && document.activeElement && document.activeElement.closest('.element') && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
3015 document.activeElement.blur();
3016 }
3017 /*if (event.keyCode!=9 && document.activeElement && document.activeElement.closest('.element') && document.activeElement.tagName=='TEXTAREA' && document.activeElement.value.length>0) {
3018 _this.focusElement(parseInt(document.activeElement.closest('.element').dataset.number));
3019 return;
3020 }*/
3021 const tf = root.querySelector('.task_focused');
3022 if (!tf) {
3023 return false;
3024 }
3025 if (event.code === "Tab") { //TAB
3026 _this.focusNextElement();
3027 }
3028 });
3029
3030 document.addEventListener('keyup', function (event) {
3031 //Удерживание кнопки не ставит вердикт, а показывает подсказку (совместно с onKey и onkeydown)
3032 clearTimeout(_this.keyHoldTimeout);
3033 if (_this.openedPopup) {
3034 _this.openedPopup.parentElement.classList.remove('openedByKey');
3035 _this.openedPopup = false;
3036 }
3037 });
3038
3039
3040 //Проставляем номера и количество тасков в листалках и биндим действия на кнопки
3041 root.style.left = '0';
3042 for (let i = 0; i < tasks.length; i++) {
3043 tasks[i].querySelector('.curTab').innerHTML = i + 1;
3044 tasks[i].querySelector('.tabsCount').innerHTML = tasks.length;
3045 tasks[i].dataset.number = i.toString();
3046 tasks[i].querySelector('.leftScroll').i = i;
3047 tasks[i].querySelector('.rightScroll').i = i;
3048
3049 //Переключение между тасками
3050 if (!verticalSuite) { //Горизонтальное расположение тасков в сьюте (с листалкой)
3051 tasks[i].querySelector('.leftScroll').addEventListener('click', function () {
3052 if (this.i > 0 && !window.scrollingNow) {
3053 window.scrollingNow = true;
3054 root.style.left = parseInt(root.style.left) + 100 + 'vw';
3055 setTimeout(function () {
3056 _this.focusTask(i - 1, false);
3057 }, 50);
3058 }
3059 });
3060 tasks[i].querySelector('.rightScroll').addEventListener('click', function () {
3061 if (this.i < tasks.length - 1 && !window.scrollingNow) {
3062 window.scrollingNow = true;
3063 root.style.left = parseInt(root.style.left) - 100 + 'vw';
3064 setTimeout(function () {
3065 _this.focusTask(i + 1, false);
3066 }, 50);
3067 }
3068 });
3069 } else { //Традиционное, вертикальное расположение тасков в сьюте
3070 tasks[i].querySelector('.leftScroll').addEventListener('click', function () {
3071 if (window.curTask > 0) {
3072 window.scrollingNow = true;
3073 setTimeout(function () {
3074 _this.focusTask(window.curTask - 1, true);
3075 }, 50);
3076 }
3077 });
3078 tasks[i].querySelector('.rightScroll').addEventListener('click', function () {
3079 if (window.curTask < tasks.length - 1) {
3080 window.scrollingNow = true;
3081 setTimeout(function () {
3082 _this.focusTask(window.curTask + 1, true);
3083 }, 50);
3084 }
3085 });
3086 }
3087
3088
3089 //Сворачивалка/разворачивалка блоков
3090 if (taskMinimizer) {
3091 Array.from(tasks[i].querySelectorAll('.block.minimizable .blockTitle:not(.tabScroller):not(.searchField):not(.darkButt)')).forEach(function (btn) {
3092 if (!isMobile) {
3093 btn.addEventListener('click', function () {
3094 if (this.parentElement.classList.contains('minimized')) {
3095 this.parentElement.classList.remove('minimized');
3096 } else {
3097 this.parentElement.classList.add('minimized');
3098 }
3099 });
3100 } else {
3101 btn.closest('.block').classList.remove('minimizable');
3102 }
3103 });
3104 }
3105 }
3106
3107
3108 //Включаем светлую тему для джедаев
3109 if (this.storage && this.storage.getItem('funfrogLightSide') === false) {
3110 for (const switchButton of root.querySelectorAll('.task .btnDarkSide a')) {
3111 switchButton.setAttribute('title', localize('lightSide'));
3112 root.classList.add('darkSide');
3113 }
3114 }
3115
3116 //Going full Anakin and back
3117 for (const switchButton of root.querySelectorAll('.task .btnDarkSide a')) {
3118 switchButton.addEventListener("click", function () {
3119 if (root.classList.contains('darkSide')) {
3120 for (const switchButton of root.querySelectorAll('.task .btnDarkSide a')) {
3121 switchButton.setAttribute('title', localize('darkSide'));
3122 }
3123 root.classList.remove('darkSide');
3124 if (_this.storage) {
3125 _this.storage.setItem('funfrogLightSide', 'true', new Date().getTime() + 30000000);
3126 }
3127 } else {
3128 for (const switchButton of root.querySelectorAll('.task .btnDarkSide a')) {
3129 switchButton.setAttribute('title', localize('lightSide'));
3130 }
3131 root.classList.add('darkSide');
3132 if (_this.storage) {
3133 _this.storage.setItem('funfrogLightSide', 'false', new Date().getTime() + 30000000);
3134 }
3135 }
3136 });
3137 }
3138
3139 //Пытаемся выпытать у браузера сохранённую солюшку, если она была
3140 //this.getSolutionsFromStorage(); Перенесено в loadStuffFromTheOneLibrary()
3141
3142 //Периодически сохраняем всю солюшку в локальное хранилище, чтобы потом, если что, можно было восстановить
3143 if (_this.storage) {
3144 let options = _this.getOptions(), assId = options.assignment._options.assignment.id,
3145 wOptions = options.workspaceOptions;
3146 if (assId && !wOptions.isReadOnly && !wOptions.isReviewMode) {
3147 setInterval(function () {
3148 let solutions = [];
3149 let solutionsUpdated = false;
3150 _this.getTasks().forEach(task => {
3151 if (task.theOneSolutionUpdated) {
3152 solutionsUpdated = true;
3153 task.theOneSolutionUpdated = false;
3154 }
3155 solutions.push(task.theOneSolution);
3156 });
3157 if (!solutionsUpdated) {
3158 return;
3159 }
3160 if (printSolution) {console.log('Сохранили солюшку:', JSON.parse(JSON.stringify(solutions)));}
3161 _this.storage.setItem('funfrogSavedSolutions_' + assId, JSON.stringify(solutions));
3162 }, 10000);
3163 }
3164 }
3165
3166 //Биндим действия на все helpContainer'ы
3167 for (const q of root.querySelectorAll('.helpContainer:not(.initialized)')) {
3168 initHelpContainer(q);
3169 }
3170
3171 document.ready.then(() => {
3172 //Обработка миспринтов во всех тасках (если они там есть)
3173 if (misprintsContainers && Array.isArray(misprintsContainers)) {
3174 for (const task of tasks) {
3175 const misprintsDiv = task.querySelector('.stringifiedMisprints');
3176 if (!misprintsDiv) {
3177 continue;
3178 }
3179
3180 const misprints = JSON.parse(misprintsDiv.innerHTML);
3181 if (!misprints || !Array.isArray(misprints)) {
3182 continue;
3183 }
3184
3185 let mString = '';
3186 for (const misprint of misprints) {
3187 mString += '|(\\s+|^)\\S?' + misprint.replace(/(ёшь|умя|ами|ому|емя|ого|ими|ишь|ёте|ите|еми|ёт|ий|ам|ые|ый|ие|am|ая|ас|ax|ит|ем|её|ей|их|ию|ею|ex|ух|ут|ёх|ум|cm|им|ую|шь|ою|ми|оё|ое|ой|ов|мя|ом|ы|и|й|о|е|ъ|у|э|а|я|м|ю|ь)$/i, '') + '\\S*';
3188 }
3189 let regExp = new RegExp('(' + mString.substr(1) + ')', 'gi');
3190
3191 for (const selector of misprintsContainers) {
3192 const div = task.querySelector(selector);
3193 if (!div) {
3194 continue;
3195 }
3196 const text = div.innerText;
3197 div.innerHTML = text.replace(regExp, '<span class="misprinted">$1</span>');
3198 }
3199 }
3200 }
3201
3202 //Завершение инициализации
3203 window.readyToKill = true;
3204 _this.focusTask(0);
3205 if (!_this.getTasks()[0].getStorageItem('__timingsStorage').started) {
3206 _this.getTasks()[0].onFocus();
3207 }
3208
3209 //Устанавливаем фокус на первый элемент
3210 /*if (hotkeysEnabled) {
3211 const elementToFocus = root.querySelector('.focusMeOnStart');
3212 if (elementToFocus) {
3213 _this.focusElement(elementToFocus.dataset.number);
3214 } else {
3215 _this.focusNextElement();
3216 }
3217 }*/
3218
3219 if (window.onSuiteReady) {onSuiteReady.apply(_this, [root]);}
3220 }).catch(err => console.error(err));
3221 },
3222
3223 //Инициализация ШВшных горячих клавиш (ТуДу: перенести в таск)
3224 initTheOneHotkeys: function () {
3225 const _this = this, root = this.getDOMElement();
3226 if (hotkeysEnabled) {
3227 this.elements = root.querySelectorAll('.element:not(.unfocusable)');
3228 for (let i = 0; i < _this.elements.length; i++) {
3229 this.elements[i].dataset.number = i.toString();
3230 if (!this.elements[i].classList.contains('hotKeysBinded')) {
3231 this.elements[i].addEventListener('click', function (e) {
3232 if (!readyToKill || !e.isTrusted) {
3233 return;
3234 }
3235 _this.focusElement(parseInt(this.dataset.number));
3236 });
3237 this.elements[i].classList.add('hotKeysBinded');
3238 }
3239 //Прорисовываем кнопкам соответствующие горячие клавиши
3240 let buttons = this.elements[i].querySelectorAll('.btn');
3241 for (let j = 0; j < buttons.length; j++) {
3242 let div = buttons[j].querySelector('.hotkeyHint');
3243 if (!div) {
3244 div = document.createElement('div');
3245 div.className = 'hotkeyHint';
3246 div.innerHTML = elementButtonsHotkeysOrderString[j];
3247 buttons[j].appendChild(div);
3248 } else {
3249 div.innerHTML = elementButtonsHotkeysOrderString[j];
3250 }
3251 }
3252 }
3253 }
3254 },
3255
3256 getSolutionsFromStorage: function () {
3257 let options = this.getOptions(), assId = options.assignment._options.assignment.id, tasks = this.getTasks();
3258 if (assId) {
3259 let sols = this.storage.getItem('funfrogSavedSolutions_' + assId);
3260 if (sols) {
3261 const tasks = this.getTasks();
3262 for (const i in tasks) {
3263 tasks[i].theOneSolution = sols[i];
3264 }
3265 if (printSolution) {console.log('Солюшка из локального хранилища:', JSON.parse(JSON.stringify(sols)));}
3266 return true;
3267 }
3268 }
3269 },
3270
3271 maybeScrollToThisElement: function (element, forceTop) { //Прокручивает страницу к элементу, если он находится за пределами экрана
3272 let root = this.getDOMElement(),
3273 what2scroll = verticalSuite ? root : (fullScreenMode ? root.querySelector('.task_focused .block.main>.blockContent') : root.querySelector('.task_focused')),
3274 topMargin = fullScreenMode ? 40 : 10;
3275 if (!forceTop && element.getBoundingClientRect().bottom + 10 > document.documentElement.clientHeight) { //Надо подкрутить страницу
3276 $(what2scroll).animate({scrollTop: what2scroll.scrollTop + element.getBoundingClientRect().bottom - document.documentElement.clientHeight + 10}, 200);
3277 } else if (forceTop || element.getBoundingClientRect().top - topMargin < 0) { //Надо подкрутить страницу
3278 $(what2scroll).animate({scrollTop: what2scroll.scrollTop + element.getBoundingClientRect().top - topMargin}, 200);
3279 }
3280 },
3281 focusNextElement: function (dontScroll) { //Пытается перевести фокус на следующий элемент
3282 if (window.thisIsMobileDevice) {return;}
3283 let root = this.getDOMElement();
3284 let _this = this;
3285 let n = (_this.focusedElement && _this.focusedElement.closest('.task_focused')) ? //Если есть элемент с фокусом в этом таске
3286 parseInt(_this.focusedElement.dataset.number) + 1 : //Берём следующий
3287 parseInt(root.querySelector('.task_focused .element:not(.unfocusable)') ? root.querySelector('.task_focused .element:not(.unfocusable)').dataset.number : null); //Или берём первый в этом таске
3288 if (!n && n !== 0) {
3289 return;
3290 }
3291 let nothing = false, nStart = n; //Запоминаем текущий номер элемента, а то вдруг по кругу пройдём: чтоб не зациклиться.
3292 while (
3293 !_this.elements[n] ||
3294 _this.elements[n].closest('.excluded') ||
3295 !_this.elements[n].closest('.task_focused') ||
3296 _this.elements[n].closest('.view.hidden') ||
3297 window.activeWindow && !isDescendant(window.activeWindow, _this.elements[n]) ||
3298 !window.activeWindow && _this.elements[n].closest('.window')
3299 ) { //Если текущий элемент спрятан или не в этом таске
3300 n++;
3301 if (n >= _this.elements.length) {
3302 n = root.querySelector('.task_focused .element:not(.unfocusable)').dataset.number;
3303 }
3304 if (n === nStart) {
3305 nothing = true;
3306 break;
3307 }
3308 }
3309 if (!nothing) {
3310 _this.focusElement(n, dontScroll);
3311 }
3312 },
3313 focusPreviousElement: function () { //Пытается перевести фокус на предыдущий элемент
3314 if (window.thisIsMobileDevice) {return;}
3315 let root = this.getDOMElement();
3316 let _this = this;
3317 let n = (_this.focusedElement && _this.focusedElement.closest('.task_focused')) ? //Если есть элемент с фокусом в этом таске
3318 parseInt(_this.focusedElement.dataset.number) - 1 : //Берём предыдущий
3319 _this.elements.length - 1; //Или берём тупо самый последний
3320 let nothing = false, nStart = n; //Запоминаем текущий номер элемента, а то вдруг по кругу пройдём: чтоб не зациклиться.
3321 while (
3322 !_this.elements[n] ||
3323 _this.elements[n].closest('.excluded') ||
3324 !_this.elements[n].closest('.task_focused') ||
3325 _this.elements[n].closest('.view.hidden') ||
3326 window.activeWindow && !isDescendant(window.activeWindow, _this.elements[n]) ||
3327 !window.activeWindow && _this.elements[n].closest('.window')
3328 ) { //Если текущий элемент спрятан или не в этом таске
3329 n--;
3330 if (n < 0) {
3331 n = _this.elements.length - 1;
3332 }
3333 if (n === nStart) {
3334 nothing = true;
3335 break;
3336 }
3337 }
3338 if (!nothing) {
3339 _this.focusElement(n);
3340 }
3341 },
3342 focusElement: function (n, dontScroll) {
3343 if (window.thisIsMobileDevice) {return;}
3344 let root = this.getDOMElement();
3345 let _this = this;
3346 n = parseInt(n);
3347
3348 document.activeElement.blur();
3349
3350 Array.from(root.querySelectorAll('.element.focused')).forEach(function (ef) {
3351 ef.classList.remove('focused');
3352 });
3353 this.focusedElement = _this.elements[n];
3354 this.focusedElement.classList.add('focused');
3355
3356 //На потестить
3357 if (!dontScroll) {
3358 this.maybeScrollToThisElement(this.focusedElement);
3359 }
3360
3361 if (this.isReadOnly || this.interfaceBlocked) {
3362 return;
3363 }
3364
3365 if (_this.focusedElement.querySelector('input')) {
3366 setTimeout(function () {
3367 const input = _this.focusedElement.querySelector('input');
3368 if (input) {input.focus();}
3369 }, 100);
3370 } else if (_this.focusedElement.querySelector('textarea')) {
3371 setTimeout(function () {
3372 _this.focusedElement.querySelector('textarea').focus();
3373 }, 100);
3374 } else if (!_this.focusedElement.buttons) {
3375 _this.focusedElement.buttons = _this.focusedElement.querySelectorAll('.btn');
3376 }
3377 },
3378 focusTask: function (i) {
3379 const _this = this;
3380 const n = parseInt(i);
3381
3382 if (!verticalSuite) {
3383 if (!this.tasks) {
3384 return;
3385 }
3386 Array.from(this.getDOMElement().querySelectorAll('.task_focused')).forEach(task => {
3387 task.classList.remove('task_focused');
3388 });
3389 this.tasks[n].classList.add('task_focused');
3390 } else {
3391 TolokaHandlebarsTaskSuite.prototype.focusTask.apply(this, arguments);
3392 }
3393
3394 if (window.curTask === n) {
3395 return;
3396 }
3397 window.curTask = n;
3398
3399 //Обновлянем все просмотрщики с канвасами
3400 for (const c of _this.tasks[window.curTask].querySelectorAll('.imageField .imageBlock canvas')) {
3401 if (c.initCanvas) {
3402 c.initCanvas();
3403 }
3404 }
3405
3406 setTimeout(function () {
3407 window.scrollingNow = false;
3408 let elementToFocus = _this.tasks[window.curTask].querySelector('.focusMeOnStart');
3409 if (!elementToFocus && _this.focusedElement && _this.focusedElement.closest('.task_focused')) {
3410 elementToFocus = _this.focusedElement;
3411 }
3412 if (elementToFocus) {
3413 _this.focusElement(elementToFocus.dataset.number, true);
3414 } else {
3415 _this.focusNextElement(true);
3416 }
3417 _this.maybeScrollToThisElement(_this.tasks[window.curTask], true);
3418 }, 250);
3419 },
3420 focusNextTask: function () {
3421 if (this.getDOMElement().querySelector('.window.opened')) {
3422 return;
3423 }
3424 this.getDOMElement().querySelector('.task_focused .rightScroll').click();
3425 },
3426 focusPreviousTask: function () {
3427 if (this.getDOMElement().querySelector('.window.opened')) {
3428 return;
3429 }
3430 this.getDOMElement().querySelector('.task_focused .leftScroll').click();
3431
3432 },
3433 initHotkeys: function () {
3434 this.hotkey.reset();
3435 this.hotkey.on('arrow-left', this.focusPreviousTask, this);
3436 this.hotkey.on('arrow-down', this.focusNextTask, this); //Почему-то это arrow-right
3437 if (!hotkeysEnabled) {
3438 this.hotkey.on('arrow-up', this.focusPreviousTask, this);
3439 this.hotkey.on('arrow-right', this.focusNextTask, this); //Почему-то это arrow-down
3440 } else {
3441 this.hotkey.on('arrow-up', this.focusPreviousElement, this);
3442 this.hotkey.on('arrow-right', this.focusNextElement, this); //Почему-то это arrow-down
3443 this.hotkey.on('key', this.onKey, this);
3444 }
3445 },
3446 onKey: function (key) {
3447 if (Date.now() - this.keydownTime > timeToOpenHintByKey) {
3448 return;
3449 }
3450 if (this.focusedElement && this.focusedElement.buttons && this.focusedElement.closest('.task_focused') && elementButtonsHotkeysOrderString.includes(key)) {
3451 let btn = this.focusedElement.buttons[elementButtonsHotkeysOrderString.indexOf(key)];
3452 if (btn) {
3453 if (btn.classList.contains('linkButton')) {
3454 btn.querySelector('a').click();
3455 } else {
3456 btn.click();
3457 }
3458 }
3459 }
3460 },
3461 validate: function () {
3462 let root = this.getDOMElement(), _this = this;
3463 if (typeof beforeTaskSuiteValidation !== "undefined") {
3464 const result = beforeTaskSuiteValidation.apply(this, [root]);
3465 if (result) {
3466 return true;
3467 }
3468 }
3469 window.noValidationErrors = true;
3470 //Перевод фокуса на активный таск (он может быть не первым, если это задание - обучение, и были указаны неправильные ответы)
3471 window.hintScrollerCounter = 0;
3472 let tf = root.querySelector('.task_focused');
3473 if (!tf) {
3474 this.focusTask(0);
3475 tf = root.querySelector('.task_focused');
3476 }
3477 this.currentFocusedTaskNumber = tf.dataset.number || 0;
3478 clearInterval(window.hintScrollTimer);
3479 window.hintScrollTimer = setInterval(function () {
3480 let curTask = root.querySelector('.task_focused');
3481 if (curTask) {
3482 if (!verticalSuite) {
3483 if (curTask.dataset.number !== _this.currentFocusedTaskNumber) {root.style.left = curTask.dataset.number * -100 + 'vw';}
3484 } else {
3485 window.scrollingNow = true;
3486 window.curTask = parseInt(curTask.dataset.number);
3487 setTimeout(function () {
3488 $(root).animate({scrollTop: curTask.offsetTop}, 200);
3489 setTimeout(function () {
3490 window.scrollingNow = false;
3491 }, 250);
3492 }, 50);
3493 }
3494 }
3495 window.hintScrollerCounter++;
3496 if (window.hintScrollerCounter > 1 || (curTask && _this.currentFocusedTaskNumber !== curTask.dataset.number)) {
3497 clearInterval(window.hintScrollTimer);
3498 }
3499 }, 1000);
3500
3501 const result = TolokaHandlebarsTaskSuite.prototype.validate.apply(this, arguments);
3502 if (printSolution) {
3503 console.log('Итоговая солюшка тасксьюта: ' , arguments[0]);
3504 }
3505 return result;
3506 }
3507};
3508
3509
3510
3511
3512
3513
3514
3515/*
3516*
3517* Хелперы Handlebars
3518*
3519*/
3520window.theOneHandlebarsHelpers = {
3521 //Рисует поля для выбора ответов
3522 drawFields: function (fields) {
3523 let context = this;
3524 //Логика такая - перебираем все поля во входном JSON и отрисовываем их. Вешать на них валидацию будем в onRender каждого таска
3525 return getFieldsHTML(fields, context);
3526 },
3527 urlDecode: function (url) {
3528 return decodeURIComponent(url.replace(/\+/g, ' '));
3529 },
3530 urlEncode: function (url) {
3531 return encodeURIComponent(url);
3532 },
3533 urlRemoveSpacesAndEncode: function (url) {
3534 return encodeURIComponent(url.replace(/\s+/g, ''));
3535 },
3536 //Выводит строки для описания входного задания - возможно, стоит удалить?
3537 drawInputStrings: function (inputStrings, placeholders) {
3538 let out = '';
3539 Array.from(inputStrings).forEach(function (inputString) {
3540 //У нас могут быть либо обычные плейсхолдеры - %%имя%%, либо со ссылкой - %%имя||ссылка%%. Меняем их тут на соответствующий код.
3541 const s = inputString.text.replace(/\$\$.*?\$\$/g, function (str) {
3542 const placeholder = str.match(/[$|][^$|]+[$|]/g);
3543 if (placeholder[1]) {
3544 return '<a href="' + placeholders[placeholder[1].match(/[$|]([^$|]+)[$|]/)[1]] + '" target="_blank">' + placeholders[placeholder[0].match(/[$|]([^$|]+)[$|]/)[1]] + '</a>';
3545 } else if (placeholder[0]) {
3546 return placeholders[placeholder[0].match(/[$|]([^$|]+)[$|]/)[1]];
3547 }
3548
3549 });
3550 out += '<div class="stepTitle">' + s + '</div>';
3551 });
3552 return out;
3553 },
3554 //Помогает передавать объекты как аргументы хелперов
3555 o: function ({hash}) {
3556 return hash;
3557 },
3558 //Устанавливает словарь локализации window.l10n
3559 setLocalization: function (l10n) {
3560 window.handlebarsContext = this;
3561 window.currentRenderingTask.handlebarsContext = this;
3562 window.l10n = l10n;
3563 },
3564 //Локализует строку в соответствии с window.l10n
3565 l: function (t) {
3566 return localize(t);
3567 },
3568 setTaskName: function (name) {
3569 window.handlebarsContext = this;
3570 window.currentRenderingTask.handlebarsContext = this;
3571 name = name ? name.length < 60 ? name : name.substr(0, 57) + '...' : 'Задание';
3572 const taskNameHTML = "<div class='taskName'>${name}</div>";
3573 return taskNameHTML;
3574 },
3575 ploosAdeen: function (someNumb) {
3576 return someNumb + 1;
3577 },
3578 shortName: function (name) {
3579 return name ? name.length < 60 ? name : name.substr(0, 57) + '...' : 'Задание';
3580 },
3581 e: function (options) {
3582 return "<div class="element ${options.hash.name ? options.hash.name : 'alien unfocusable'} ${options.hash.class}" ${options.hash.HTMLTitle ? "title="${options.hash.HTMLTitle}"" : ''}>${options.fn(this)}</div>";
3583 },
3584 a: function (options) {
3585 if (!options.hash.title) {
3586 options.hash.title = options.hash.href;
3587 }
3588 if (!options.hash.name) {
3589 options.hash.name = '';
3590 }
3591 if (!options.hash.class) {
3592 options.hash.class = '';
3593 }
3594 return "<span class="link ${options.hash.name} ${options.hash.class}"><a data-name="${options.hash.name}" target="_blank" href="${options.hash.href}" rel="noreferrer"><i class='ffIcon' data-icon="open_in_new"></i>${options.hash.title}</a></span>";
3595 },
3596 window: function (options) {
3597 return "<div class="window ${options.hash.name} ${options.hash.class}">${options.fn(this)}</div>";
3598 },
3599 v: function (options) {
3600 return "<div class="view ${options.hash.name} ${options.hash.class}" data-name="${options.hash.name}">${options.fn(this)}</div>";
3601 },
3602 flexBox: function (options) {
3603 return "<div class="flexBox ${options.hash.name} ${options.hash.class}" ${options.hash.name ? "data-name="${options.hash.name}"" : ''}>${options.fn(this)}</div>";
3604 },
3605 spoiler: function (options) {
3606 return "<div class="element spoiler ${options.hash.name ? options.hash.name : 'alien'} ${options.hash.class}" data-name="${options.hash.name}">
3607 <div class='spoilerHead'>
3608 ${getSwitchButtonHTML({
3609 icon: 'angle_down',
3610 caption: options.hash.title
3611 })}
3612 </div>
3613 <div class='spoilerContainer'>
3614 <div class='spoilerContent'>
3615 ${options.fn(this)}
3616 </div>
3617 </div>
3618 </div>";
3619 },
3620 iLine: function (options) {
3621 if (!options.hash.name) {
3622 options.hash.name = '';
3623 }
3624 if (!options.hash.title) {
3625 options.hash.title = '';
3626 }
3627 return "<div class="infoLine ${options.hash.name} ${options.hash.class}">" +
3628 "<span class='ilTitle'>${options.hash.title}:</span>" +
3629 "<span class='ilContent'>${options.fn(this)}</span>" +
3630 "</div>";
3631 },
3632 iLines: function (options) {
3633 if (!options.hash.name) {
3634 options.hash.name = '';
3635 }
3636 if (!options.hash.title) {
3637 options.hash.title = '';
3638 }
3639 return "<div class="infoLine infoLines ${options.hash.name} ${options.hash.class}">" +
3640 "<span class='ilTitle'>${options.hash.title}:</span>" +
3641 "<span class='ilContent'>${options.fn(this)}</span>" +
3642 "</div>";
3643 },
3644 baloon: function (options) {
3645 const opacity = options.hash.opacity ? options.hash.opacity : 20;
3646 const bdOpacity = Math.min(255, parseInt(opacity, 16) + 40).toString(16);
3647 options.hash.color = '#' + options.hash.color;
3648 return "<div class="baloon ${options.hash.name} ${options.hash.class}" style="border-color: ${options.hash.color}${bdOpacity}; background-color: ${options.hash.color}${opacity};">" +
3649 "${options.hash.title ? "<div class="baloonTitle">${options.hash.title}</div>" : ''}" +
3650 "${options.fn(this)}" +
3651 "</div>";
3652 },
3653 block: function (options) {
3654 let blockButtons = '';
3655 if (!options.hash.class) {options.hash.class = '';}
3656 if (taskMinimizer && !options.hash.class.includes('nonMinimizable') && (!fullScreenMode || fullScreenMode && !options.hash.class.includes('main'))) {
3657 options.hash.class += ' minimizable';
3658 }
3659
3660 if (options.hash.class.includes('main')) {
3661 if (useComplainter) {
3662 blockButtons += "
3663 <div class='blockTitle btnComplainter'>
3664 <a title="${localize('complaint')}">
3665 <i class="ffIcon" data-icon="comment_smile"></i>
3666 </a>
3667 </div>";
3668 }
3669 if (useGlossarySearch) {
3670 blockButtons += "
3671 <div class='blockTitle btnGlossarySearch noHotkeys'>
3672 <a title="${localize('glossarySearch')}">
3673 <i class="ffIcon" data-icon="helpRound"></i>
3674 </a>
3675 <div class='glossarySearchForm alien'></div>
3676 </div>";
3677 }
3678 blockButtons += "
3679 <div class='blockTitle darkButt btnDarkSide'>
3680 <a title="${localize('darkSide')}">
3681 <i class="ffIcon" data-icon="invert_colors"></i>
3682 </a>
3683 </div>";
3684
3685 if (window.customTitleElements) {
3686 for (const e of customTitleElements) {
3687 blockButtons += "
3688 <div class='blockTitle custom ${e.name} ${e.class ? e.class : ''}'>
3689 ${Handlebars.compile(e.content)(this)}
3690 </div>";
3691 }
3692 }
3693
3694 blockButtons += "
3695 <div class='blockTitle tabScroller'>
3696 <span class='leftScroll'>
3697 <i class="ffIcon" data-icon="keyboard_backspace"></i>
3698 <i class="ffIcon" data-icon="keyboard_tab"></i>
3699 </span>
3700 <span class="tabNumbers">
3701 <span class='curTab'></span>
3702 /
3703 <span class='tabsCount'></span>
3704 </span>
3705 <span class='rightScroll'>
3706 <i class="ffIcon" data-icon="keyboard_backspace"></i>
3707 <i class="ffIcon" data-icon="keyboard_tab"></i>
3708 </span>
3709 <div class='tabList'></div>
3710 </div>
3711 ";
3712 }
3713
3714 let title = '';
3715 if (options.hash.title) {
3716 title = "<div class='blockTitle'>${options.hash.title}<span class='miniMaxi' title='Свернуть/развернуть блок'><i class="material-icons">keyboard_arrow_up</i></span></div>";
3717 }
3718
3719 // свернуть блок, если блоку назначен класс minimized
3720 let minimized = '';
3721 if (options.hash.class.includes('minimized')) {
3722 minimized = 'minimized';
3723 options.hash.class = options.hash.class.replace('minimized', '');
3724 }
3725
3726 //Подсказка
3727 let help = '';
3728 if (options.hash.help) {
3729 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${options.hash.help}</div></div>";
3730 }
3731
3732 return "<div class='block ${options.hash.name} ${options.hash.class}' ${options.hash.name ? "data-name="${options.hash.name}"" : ''}><div class='blockContent ${minimized}'>${help}<div class='blockButtons'>${blockButtons}</div>${title}${options.fn(this)}</div></div>";
3733 },
3734 //Рисует баннер для шаблонов Директа
3735 drawBanner: function (banner) {
3736 if (!banner) {
3737 return;
3738 }
3739 const b = banner.data[0];
3740 if (!b) {
3741 return;
3742 }
3743
3744 const title = b.title + (b.title_extension ? ' - ' + b.title_extension : '');
3745 const domain = /https?:\/\/([^/]+)/.exec(b.parametrized_href)[1];
3746
3747 const out =
3748 '<div class="element alien unfocusable bannerElement">' +
3749 '<div class="bannerContainer">' +
3750 '<div class="banner">' +
3751 '<div class="bannerTitle">' +
3752 '<i class="ffIcon" data-icon="eye"></i><a target="_blank" class="unclicked" href="' + b.parametrized_href + '" rel="noreferrer" data-domain="' + (domain ? domain : b.parametrized_href) + '">' + title + '</a>' +
3753 '</div>' +
3754 '<div class="bannerText">' + b.body + '</div>' +
3755 '<div class="bannerDomain">' + b.domain + '</div>' +
3756 '</div>' +
3757 '</div>' +
3758 '</div>';
3759
3760 return out;
3761 },
3762 //Рисует дополнительные данные баннера для шаблонов Директа
3763 drawBannerInfo: function (banner) {
3764 if (!banner) {
3765 return;
3766 }
3767 const b = banner.data[0];
3768 const m = banner.meta;
3769 if (!b || !m) {
3770 return;
3771 }
3772
3773 let infoLines = '';
3774
3775 function addInfoLine(title, lineClass, value, valueType, additional) {
3776 let v;
3777
3778 if (!valueType) {
3779 valueType = 'text';
3780 }
3781
3782 switch (valueType) {
3783 case 'link':
3784 v = '<a target="_blank" href="' + additional + '" rel="noreferrer">' + value + '</a>';
3785 break;
3786 case 'somethingOther':
3787 break;
3788 default:
3789 v = value;
3790 }
3791
3792 const line =
3793 '<div class="bannerInfoLine ' + lineClass + '">' +
3794 '<div class="bilTitle">' +
3795 title +
3796 '</div>' +
3797 '<div class="bilValue ' + valueType + '">' +
3798 v +
3799 '</div>' +
3800 '</div>';
3801
3802 infoLines += line;
3803 }
3804
3805 function arrayOrStringToString(something) {
3806 if (Array.isArray(something)) {
3807 let list = '';
3808 for (const item of something) {
3809 list += ', ' + item;
3810 }
3811 list = list.substr(2);
3812 return list;
3813 } else if (something) {
3814 return something;
3815 }
3816 }
3817
3818 //Регионы показа
3819 if (b.geo_country) {
3820 addInfoLine('Регионы показа:', 'geoCountry', arrayOrStringToString(b.geo_country));
3821 }
3822 //ГеоТекст
3823 //if (b.geo_text) {addInfoLine('Регион подробно:', 'geoText', b.geo_text);}
3824 //Языки
3825 if (b.lang) {
3826 addInfoLine('Языки:', 'lang', arrayOrStringToString(b.lang));
3827 }
3828 //Логин
3829 if (b.login) {
3830 addInfoLine('Логин:', 'login', b.login);
3831 }
3832 //Просмотр баннера
3833 if (m.banner_id) {
3834 addInfoLine('Просмотр баннера:', 'bannerLink', m.banner_id, 'link', 'https://direct-mod.yandex-team.ru/history/banner/?bid=' + m.banner_id);
3835 }
3836 //Просмотр кампании
3837 if (m.campaign_id) {
3838 addInfoLine('Просмотр кампании:', 'campaignLink', m.campaign_id, 'link', 'https://direct-mod.yandex-team.ru/campaign/show/?cid=' + m.campaign_id);
3839 }
3840
3841 const out =
3842 '<div class="element alien unfocusable bannerInfoElement">' +
3843 '<div class="bannerInfoContainer">' +
3844 '<div class="bannerInfo">' +
3845 infoLines +
3846 '</div>' +
3847 '</div>' +
3848 '</div>';
3849
3850 return out;
3851 },
3852 misprints: function (misprints) {
3853 if (!misprints) {
3854 return;
3855 }
3856 const stringifiedMisprints =
3857 '<div class="stringifiedMisprints" style="display: none;">' +
3858 JSON.stringify(misprints) +
3859 '</div>';
3860 return stringifiedMisprints;
3861 }
3862};
3863
3864
3865
3866
3867
3868
3869
3870/*
3871*
3872* Всякие функции
3873*
3874*/
3875window.dumpObjectKeysToDocumentBody = (obj) => {
3876 document.querySelector('body').innerHTML+='<br><br>';
3877 for (const k in obj) {
3878 document.querySelector('body').innerHTML+= k + '<br>';
3879 }
3880};
3881
3882window.loadStuffFromTheOneLibrary = ( assignment ) => {
3883 //Вытаскиваем настройки шаблона из объекта
3884 for (const s in window.theOneSettings) {
3885 window[s] = window.theOneSettings[s];
3886 }
3887 //Обогащаем таск и тасксьют методами из библиотеки и накидываем поверх кастомных методов, ежели таковые имеются
3888 const taskSuite = assignment.getTaskSuite();
3889 injectMethods(theOneAssignmentMethods, assignment, window.customAssignmentMethods);
3890 injectMethods(theOneTaskSuitePrototype, taskSuite, window.customTaskSuiteMethods);
3891 for (const task of taskSuite.getTasks()) {
3892 injectMethods(theOneTaskPrototype, task, window.customTaskMethods);
3893 }
3894 if (!window.helpersRegisteredAndCssApplied) {
3895 //Регистрируем библиотечные хелперы Handlebars
3896 for (const name in theOneHandlebarsHelpers) {
3897 Handlebars.registerHelper(name, theOneHandlebarsHelpers[name]);
3898 }
3899 //Вставляем CSS
3900 applyTheOneCSS();
3901 window.helpersRegisteredAndCssApplied = true;
3902 }
3903 //То, что раньше было в onResume()
3904 window.lang = assignment.getWorkspaceOptions().language || 'ru';
3905 //Инициализация жалобщика
3906 if (useComplainter) {
3907 $.getScript(complainterLibSrc, function (data, textStatus, jqxhr) {
3908 let options = assignment.getWorkspaceOptions();
3909 let namespace = complainterNamespace;
3910
3911 window.Complaints.default.initialize({
3912 api: {
3913 url: '',
3914 origin: options.origin,
3915 proxyName: complainterProxyName,
3916 assignmentId: assignment.getId()
3917 },
3918 language: options.language,
3919 namespace,
3920 ajax: function (ajaxOptions) {
3921 !(ajaxOptions.type) && (ajaxOptions.type = ajaxOptions.method);
3922 return assignment.getSandboxChannel().request('proxy', ajaxOptions);
3923 },
3924
3925 custom_causes: complainterCauses,
3926
3927 custom_translations: complainterTranslations,
3928 });
3929 });
3930 }
3931 //Пытаемся выпытать у браузера сохранённую солюшку, если она была
3932 assignment.getTaskSuite().getSolutionsFromStorage();
3933};
3934
3935window.injectMethods = (from, to, customMethods) => {
3936 for (const m in from) {
3937 to[m] = from[m];
3938 }
3939 if (customMethods) {
3940 for (const m in customMethods) {
3941 if (to[m]) {
3942 to['_' + m] = to[m];
3943 }
3944 to[m] = customMethods[m];
3945 }
3946 }
3947};
3948
3949window.applyTheOneCSS = () => {
3950 const style = document.createElement('style');
3951 style.setAttribute('type', 'text/css');
3952 style.innerHTML = theOneCSS;
3953 document.querySelector('head').appendChild(style);
3954};
3955
3956window.initHelpContainer = (q) => {
3957 const popUpTitle = q.querySelector('.popUpTitle');
3958 popUpTitle.innerHTML = "<div class="popUpTitleInner">${popUpTitle.innerHTML}</div>";
3959
3960 q.addEventListener('click', function () {
3961 if (this.classList.contains('triggered')) {
3962 this.classList.remove('triggered');
3963 } else {
3964 this.classList.add('triggered');
3965 makeSureThisDivFitsOnScreen(q.querySelector('.popUpTitle'));
3966 }
3967 });
3968 q.classList.add('initialized');
3969};
3970
3971//Проверяет, помещается ли указанный контейнер на экране, и если нет - сдвигает его и масштабирует
3972window.makeSureThisDivFitsOnScreen = (div) => {
3973 div.setAttribute('style', '');
3974
3975 const
3976 style = getComputedStyle(div),
3977 margin = 20, //Ровно на столько пикселей от края экрана будут отодвигаться подсказки
3978 task = fullScreenMode ? div.closest('.block.main') || div.closest('.windowContent') || div.closest('.task') : div.closest('.windowContent') || div.closest('.task');
3979 let
3980 verticalProperty,
3981 n = 0;
3982
3983
3984 do {
3985 if (n++ > 20) {break;}
3986
3987
3988 let
3989 divRect = div.getBoundingClientRect(),
3990 taskRect = task.getBoundingClientRect(),
3991 divEnd;
3992
3993 //console.log(divRect);
3994 //console.log(taskRect);
3995
3996 //Ширина блока должна быть меньше ширины таска
3997 if (divRect.width > taskRect.width - 2*margin) {
3998 div.style.maxWidth = taskRect.width - 2*margin + "px";
3999 divRect = div.getBoundingClientRect();
4000 taskRect = task.getBoundingClientRect();
4001
4002 }
4003
4004 //Высота блока должна быть меньше высоты таска
4005 if (divRect.height > taskRect.height - 2*margin) {
4006 div.style.maxHeight = taskRect.height - 2*margin + "px";
4007 divRect = div.getBoundingClientRect();
4008 taskRect = task.getBoundingClientRect();
4009 }
4010
4011 //Выход за правую границу
4012 divEnd = divRect.right + margin;
4013 if (divEnd > taskRect.right - 20) {
4014 div.style.left = "-" + (divEnd - taskRect.right).toString() + "px";
4015 divRect = div.getBoundingClientRect();
4016 taskRect = task.getBoundingClientRect();
4017 }
4018
4019 //Выход за нижнюю границу
4020 divEnd = divRect.bottom + margin;
4021 if (!div.classList.contains('errorMsgBlock') && divEnd > taskRect.bottom+10) {
4022 div.style.top = "-" + (divEnd - taskRect.bottom - parseInt(style.top)).toString() + "px";
4023 divRect = div.getBoundingClientRect();
4024 taskRect = task.getBoundingClientRect();
4025 }
4026
4027 //Выход за левую границу
4028 divEnd = divRect.left - margin;
4029 if (divEnd < -10) {
4030 div.style.left = margin + "px";
4031 divRect = div.getBoundingClientRect();
4032 taskRect = task.getBoundingClientRect();
4033 }
4034
4035 //Выход за верхнюю границу
4036 divEnd = divRect.top - margin;
4037 if (!div.classList.contains('errorMsgBlock') && divEnd < taskRect.top-10) {
4038 if (div.classList.contains('errorMsgBlock')) {
4039 div.style.maxHeight = divRect.height + divEnd - taskRect.top + "px";
4040 } else {
4041 div.style.top = "-" + (divEnd - taskRect.top - parseInt(style.top)).toString() + "px";
4042 }
4043 divRect = div.getBoundingClientRect();
4044 taskRect = task.getBoundingClientRect();
4045 }
4046
4047 } while (n<3);
4048};
4049
4050window.initImageField = (imageField) => {
4051 /*
4052 Тут 2 канваса, внешний и внутренний.
4053 img - внутренний канвас с картинкой pic
4054 canvas - внешний канвас.
4055 */
4056 let imageSource = imageField.querySelector('.imageSource'),
4057 imageOrigLink = imageField.querySelector('.imageOriginalLink'),
4058 imageBlock = imageField.querySelector('.imageBlock'),
4059 imageConsole = imageField.querySelector('.imageConsole'),
4060 noCanvasMode = imageField.classList.contains('noCanvas'),
4061 imgRatio, canvasRatio, imgD, leftButtsJSON;
4062 const
4063 leftButts = imageField.querySelector('.imageLeftButts'),
4064 rightButts = imageField.querySelector('.imageRightButts');
4065
4066
4067 if (!imageSource) {
4068 ce('imageField_no_imageSource_div', imageField.className);
4069 return;
4070 }
4071 imageSource = imageSource.innerHTML;
4072 if (!imageSource) {
4073 ce('imageField_no_imageSource', imageField.className);
4074 return;
4075 }
4076
4077 if (imageOrigLink) {
4078 imageOrigLink = imageOrigLink.innerHTML;
4079 }
4080
4081
4082 if (noCanvasMode) {
4083 let img = document.createElement('img');
4084 img.className = 'justAnImage';
4085 img.setAttribute('src', imageSource);
4086 imageBlock.appendChild(img);
4087
4088 leftButtsJSON = {
4089 type: "actionButtonGroup",
4090 class: "imageFieldRightControls",
4091 flags: ["unfocusable"],
4092 buttons: [
4093 {
4094 iconClass: "expandButtIcon",
4095 hint: "Развернуть на весь экран.\nДвойной клик по изображению делает то же самое."
4096 }
4097 ]
4098 };
4099
4100 leftButts.innerHTML = handleFieldAndGetHTML(leftButtsJSON);
4101
4102 let btnA;
4103 btnA = leftButts.querySelector('.expandButtIcon a');
4104 if (btnA) {
4105 let _btnA = btnA;
4106 btnA.addEventListener('click', expandStuff);
4107 imageBlock.addEventListener('dblclick', expandStuff);
4108
4109 function expandStuff(e) {
4110 e.preventDefault();
4111 imageField.classList.toggle('expanded');
4112 const btn = _btnA.closest('.btn');
4113 if (imageField.classList.contains('expanded')) {
4114 btn.classList.add('selected');
4115 } else {
4116 btn.classList.remove('selected');
4117 }
4118 }
4119 }
4120 } else {
4121 const canvas = imageBlock.querySelector('canvas');
4122 const ctx = canvas.getContext("2d");
4123
4124 //Внутренний канвас с картинкой
4125 const img = document.createElement('canvas');
4126 const imgCtx = img.getContext("2d");
4127
4128 ctx.imageSmoothingEnabled = false;
4129 ctx.imageSmoothingEnabled = false;
4130
4131 function initCanvas() {
4132 ctx.canvas.width = canvas.offsetWidth;
4133 ctx.canvas.height = canvas.offsetHeight;
4134 ctx.canvas.halfWidth = canvas.offsetWidth / 2;
4135 ctx.canvas.halfHeight = canvas.offsetHeight / 2;
4136 canvas.scale =/* canvas.scale ? canvas.scale :*/ Math.min(canvas.width / img.width, canvas.height / img.height);
4137 canvas.minScale = /*canvas.minScale ? canvas.minScale : */canvas.scale;
4138 img.rotation = img.rotation ? img.rotation : 0;
4139 img.left = img.width / -2;
4140 img.top = img.height / -2;
4141 onScaleChange();
4142 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4143 ctx.drawImage(img, img.left, img.top);
4144 imgRatio = img.width / img.height;
4145 canvasRatio = canvas.width / canvas.height;
4146 if (imgRatio < canvasRatio) {
4147 imgD = 1;
4148 } else {
4149 imgD = -1;
4150 }
4151 if (imgRatio < 1) {
4152 imgD *= -1;
4153 }
4154 }
4155
4156 let pic = new Image();
4157 pic.src = imageSource;
4158 imgLog('Загружаю изображение...');
4159 pic.onload = function () {
4160 imgLog('Изображение загружено!');
4161 const picMaxD = Math.max(pic.width, pic.height);
4162 const picDiagonal = Math.ceil(Math.sqrt(Math.pow(pic.width, 2) + Math.pow(pic.height, 2)));
4163 img.width = pic.width;
4164 img.height = pic.height;
4165 img.halfWidth = img.width / 2;
4166 img.halfHeight = img.height / 2;
4167 img.scale = 1;
4168 pic.left = 0 - pic.width / 2;
4169 pic.top = 0 - pic.height / 2;
4170 imgCtx.setTransform(img.scale, 0, 0, img.scale, img.halfWidth, img.halfHeight);
4171 imgCtx.drawImage(pic, pic.left, pic.top);
4172
4173 initCanvas();
4174 canvas.initCanvas = initCanvas;
4175 window.addEventListener('resize', initCanvas);
4176
4177 let btnA;
4178 btnA = leftButts.querySelector('.expandButtIcon a');
4179 if (btnA) {
4180 let _btnA = btnA;
4181 btnA.addEventListener('click', expandStuff);
4182 imageBlock.addEventListener('dblclick', expandStuff);
4183
4184 function expandStuff(e) {
4185 e.preventDefault();
4186 if (!imageField.classList.contains('expanded')) {
4187
4188 }
4189 const btn = _btnA.closest('.btn');
4190 if (!imageField.classList.contains('expanded')) {
4191 imageBlock.widthBeforeExpanding = getComputedStyle(imageBlock).width;
4192 imageBlock.heightBeforeExpanding = getComputedStyle(imageBlock).height;
4193 imageField.classList.add('expanded');
4194 btn.classList.add('selected');
4195 initCanvas();
4196 } else {
4197 btn.classList.remove('selected');
4198 imageBlock.style.width = imageBlock.widthBeforeExpanding;
4199 imageBlock.style.height = imageBlock.heightBeforeExpanding;
4200 imageField.classList.remove('expanded');
4201 initCanvas();
4202 setTimeout(() => {
4203 imageBlock.style.width = 'initial';
4204 imageBlock.style.height = 'initial';
4205 }, 100);
4206 }
4207 }
4208 }
4209
4210
4211 //Поворот влево на 90 градусов
4212 btnA = leftButts.querySelector('.undoButtIcon a');
4213 if (btnA) {
4214 btnA.addEventListener('click', function (e) {
4215 e.preventDefault();
4216 img.rotation = Math.ceil(img.rotation * 2) / 2;
4217 img.rotation > 0 ? img.rotation -= 0.5 : img.rotation = 1.5;
4218 canvas.savedScale = canvas.scale * img.scale;
4219 rotatePic();
4220 imgLog("Угол поворота изображения: ${img.rotation * 180}°");
4221 });
4222 }
4223
4224 //Поворот вправо на 90 градусов
4225 btnA = leftButts.querySelector('.redoButtIcon a');
4226 if (btnA) {
4227 btnA.addEventListener('click', function (e) {
4228 e.preventDefault();
4229 img.rotation = Math.floor(img.rotation * 2) / 2;
4230 img.rotation < 2 ? img.rotation += 0.5 : img.rotation = 0.5;
4231 canvas.savedScale = canvas.scale * img.scale;
4232 rotatePic();
4233 imgLog("Угол поворота изображения: ${img.rotation * 180}°");
4234 });
4235 }
4236
4237 //Кнопка свободного вращения
4238 let lastClickedTime;
4239 btnA = leftButts.querySelector('.syncButtIcon a');
4240 if (btnA) {
4241 btnA.addEventListener('click', function (e) {
4242 e.preventDefault();
4243 const btn = this.closest('.btn');
4244 if (!btn.classList.contains('selected')) {
4245 disableButts();
4246 btn.classList.add('selected');
4247 canvas.savedScale = canvas.scale * img.scale;
4248 img.freeRotationMode = true;
4249 } else {
4250 disableButts();
4251 }
4252
4253 //Проверка на двойной клик
4254 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4255 //Это был двойной клик, значит надо сбросить значение данного свойства
4256 img.rotation = 0;
4257 rotatePic();
4258 btn.classList.remove('selected');
4259 img.freeRotationMode = false;
4260 }
4261 //Запоминаем время включения для отслеживания двойного клика
4262 lastClickedTime = new Date().getTime();
4263 });
4264 }
4265
4266 //Инверсия изображения
4267 btnA = leftButts.querySelector('.adjustSolidButtIcon a');
4268 if (btnA) {
4269 btnA.addEventListener('click', function (e) {
4270 e.preventDefault();
4271 const btn = this.closest('.btn');
4272 if (!btn.classList.contains('selected')) {
4273 btn.classList.add('selected');
4274 img.filters.invert = 100;
4275 imgLog("Изображение инвертировано");
4276 } else {
4277 btn.classList.remove('selected');
4278 img.filters.invert = 0;
4279 imgLog("Изображение ревертировано");
4280 }
4281 applyFilters();
4282 });
4283 }
4284
4285 //Оттенок изображения
4286 btnA = leftButts.querySelector('.swatchBookIcon a');
4287 if (btnA) {
4288 btnA.addEventListener('click', function (e) {
4289 e.preventDefault();
4290 const btn = this.closest('.btn');
4291 if (!btn.classList.contains('selected')) {
4292 disableButts();
4293 btn.classList.add('selected');
4294 img.freeHueMode = true;
4295 } else {
4296 disableButts();
4297 }
4298
4299 //Проверка на двойной клик
4300 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4301 //Это был двойной клик, значит надо сбросить значение данного свойства
4302 btn.classList.remove('selected');
4303 img.freeHueMode = false;
4304 img.filters.hue = 0;
4305 applyFilters();
4306 imgLog("Оттенок восстановлен");
4307 }
4308 //Запоминаем время включения для отслеживания двойного клика
4309 lastClickedTime = new Date().getTime();
4310 });
4311 }
4312
4313 //Контрастность изображения
4314 btnA = leftButts.querySelector('.adjustButtIcon a');
4315 if (btnA) {
4316 btnA.addEventListener('click', function (e) {
4317 e.preventDefault();
4318 const btn = this.closest('.btn');
4319 if (!btn.classList.contains('selected')) {
4320 disableButts();
4321 btn.classList.add('selected');
4322 img.freeContrastMode = true;
4323 } else {
4324 disableButts();
4325 }
4326
4327 //Проверка на двойной клик
4328 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4329 //Это был двойной клик, значит надо сбросить значение данного свойства
4330 btn.classList.remove('selected');
4331 img.freeContrastMode = false;
4332 img.filters.contrast = 100;
4333 applyFilters();
4334 imgLog("Контрастность восстановлена");
4335 }
4336 //Запоминаем время включения для отслеживания двойного клика
4337 lastClickedTime = new Date().getTime();
4338 });
4339 }
4340
4341 //Яркость изображения
4342 btnA = leftButts.querySelector('.sunButtIcon a');
4343 if (btnA) {
4344 btnA.addEventListener('click', function (e) {
4345 e.preventDefault();
4346 const btn = this.closest('.btn');
4347 if (!btn.classList.contains('selected')) {
4348 disableButts();
4349 btn.classList.add('selected');
4350 img.freeBrightnessMode = true;
4351 } else {
4352 disableButts();
4353 }
4354
4355 //Проверка на двойной клик
4356 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4357 //Это был двойной клик, значит надо сбросить значение данного свойства
4358 btn.classList.remove('selected');
4359 img.freeBrightnessMode = false;
4360 img.filters.brightness = 100;
4361 applyFilters();
4362 imgLog("Яркость восстановлена");
4363 }
4364 //Запоминаем время включения для отслеживания двойного клика
4365 lastClickedTime = new Date().getTime();
4366 });
4367 }
4368
4369 //Насыщенность изображения
4370 btnA = leftButts.querySelector('.paletteButtIcon a');
4371 if (btnA) {
4372 btnA.addEventListener('click', function (e) {
4373 e.preventDefault();
4374 const btn = this.closest('.btn');
4375 if (!btn.classList.contains('selected')) {
4376 disableButts();
4377 btn.classList.add('selected');
4378 img.freeSaturationMode = true;
4379 } else {
4380 disableButts();
4381 }
4382
4383 //Проверка на двойной клик
4384 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4385 //Это был двойной клик, значит надо сбросить значение данного свойства
4386 btn.classList.remove('selected');
4387 img.freeSaturationMode = false;
4388 img.filters.saturation = 100;
4389 applyFilters();
4390 imgLog("Насыщенность восстановлена");
4391 }
4392 //Запоминаем время включения для отслеживания двойного клика
4393 lastClickedTime = new Date().getTime();
4394 });
4395 }
4396
4397 //Стирание фильтров
4398 btnA = leftButts.querySelector('.eraserButtIcon a');
4399 if (btnA) {
4400 btnA.addEventListener('click', function (e) {
4401 e.preventDefault();
4402 disableButts();
4403 img.filters = {
4404 invert: 0,
4405 hue: 0,
4406 contrast: 100,
4407 brightness: 100,
4408 saturation: 100
4409 };
4410 applyFilters();
4411 imgLog("Все фильтры стёрты");
4412 });
4413 }
4414
4415 };
4416
4417 canvas.addEventListener('mousedown', imgDrag);
4418 canvas.addEventListener('mousemove', imgMove);
4419 canvas.addEventListener('mouseup', imgDrop);
4420 canvas.addEventListener('mouseleave', imgDrop);
4421 canvas.addEventListener('wheel', imgWheel);
4422
4423
4424 function disableButts() {
4425 Array.from(leftButts.querySelectorAll('.btn.selected:not(.adjustSolidButtIcon):not(.expandButtIcon)')).forEach(btn => btn.classList.remove('selected'));
4426 ['freeRotationMode', 'freeHueMode', 'freeContrastMode', 'freeBrightnessMode', 'freeSaturationMode'].forEach(name => img[name] = false);
4427 }
4428
4429 function clrscr() {
4430 ctx.clearRect(canvas.left, canvas.top, canvas.scaledWidth, canvas.scaledHeight);
4431 }
4432
4433 function imgClrscr() {
4434 imgCtx.clearRect(-img.halfWidth - 10, -img.halfHeight - 10, img.width + 20, img.height + 20);
4435 }
4436
4437 function imgLog(text) {
4438 imageConsole.style.opacity = "0.8";
4439 imageConsole.innerHTML = text;
4440 clearTimeout(imageConsole.timeout);
4441 imageConsole.timeout = setTimeout(function () {
4442 imageConsole.style.opacity = "0";
4443 }, 2000);
4444 }
4445
4446 img.filters = {
4447 invert: 0,
4448 hue: 0,
4449 contrast: 100,
4450 brightness: 100,
4451 saturation: 100
4452 };
4453
4454 function applyFilters(f) {
4455 if (!f) {
4456 f = img.filters;
4457 }
4458 imgCtx.filter = "invert(${f.invert}%) hue-rotate(${f.hue}deg) contrast(${f.contrast}%) brightness(${f.brightness}%) saturate(${f.saturation}%)";
4459 imgCtx.drawImage(pic, pic.left, pic.top);
4460 ctx.drawImage(img, img.left, img.top);
4461 }
4462
4463
4464 function rotatePic() {
4465 clrscr();
4466 imgClrscr();
4467
4468 let angle = img.rotation * 180 % 180;
4469 angle = angle > 90 ? 90 - (angle - 90) : angle;
4470 let alpha = Math.atan2(img.height, img.width); // Угол диагонали
4471 let alpha_1 = alpha + angle * Math.PI / 180; // Новый угол первой диагонали
4472 let alpha_2 = alpha_1 + Math.PI - 2 * alpha; // Новый угол второй диагонали
4473 let H_new = Math.max(Math.abs(Math.sin(alpha_1)), Math.abs(Math.sin(alpha_2))) * Math.sqrt(img.width * img.width + img.height * img.height); // Длина проекции на ось Y
4474 let W_new = Math.max(Math.abs(Math.cos(alpha_1)), Math.abs(Math.cos(alpha_2))) * Math.sqrt(img.width * img.width + img.height * img.height); // Длина проекции на ось X
4475
4476
4477 img.scale = 1 / Math.max(W_new / img.width, H_new / img.height);
4478 if (canvas.scale > canvas.minScale || imgD < 0) {
4479 canvas.scale = canvas.savedScale / img.scale;
4480 }
4481
4482 imgCtx.setTransform(img.scale, 0, 0, img.scale, img.halfWidth, img.halfHeight);
4483 imgCtx.rotate(img.rotation * Math.PI);
4484 imgCtx.drawImage(pic, pic.left, pic.top);
4485
4486 onScaleChange();
4487 positionCorrector();
4488 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4489 clrscr();
4490 ctx.drawImage(img, img.left, img.top);
4491 imgLog("Угол поворота изображения: ${Math.round(img.rotation * 180)}°");
4492 }
4493
4494 function imgDrag(e) {
4495 canvas.midnightMover = true;
4496 imageField.classList.add('dragging');
4497 }
4498
4499 function imgMove(e) {
4500 if (!canvas.midnightMover) {
4501 return;
4502 }
4503
4504 img.left += e.movementX / canvas.scale;
4505 img.top += e.movementY / canvas.scale;
4506 positionCorrector();
4507 clrscr();
4508 ctx.drawImage(img, img.left, img.top);
4509
4510 }
4511
4512 function imgDrop() {
4513 canvas.midnightMover = false;
4514 imageField.classList.remove('dragging');
4515 }
4516
4517 function onScaleChange() {
4518 canvas.left = -canvas.halfWidth / canvas.scale;
4519 canvas.top = -canvas.halfHeight / canvas.scale;
4520 canvas.scaledWidth = canvas.width / canvas.scale;
4521 canvas.scaledHeight = canvas.height / canvas.scale;
4522 canvas.right = -canvas.left;
4523 canvas.bottom = -canvas.top;
4524 onPositionChange();
4525 }
4526
4527 function onPositionChange() {
4528 img.right = img.left + img.width;
4529 img.bottom = img.top + img.height;
4530 }
4531
4532 function positionCorrector() {
4533 onPositionChange();
4534 if (img.width > canvas.scaledWidth) {
4535 if (img.right > canvas.right && img.left > canvas.left) {
4536 img.left = canvas.left;
4537 }
4538 if (img.left < canvas.left && img.right < canvas.right) {
4539 img.left = canvas.right - img.width;
4540 }
4541 } else {
4542 if (img.left < canvas.left) {
4543 img.left = canvas.left;
4544 }
4545 if (img.right > canvas.right) {
4546 img.left = canvas.right - img.width;
4547 }
4548 }
4549 if (img.height > canvas.scaledHeight) {
4550 if (img.bottom > canvas.bottom && img.top > canvas.top) {
4551 img.top = canvas.top;
4552 }
4553 if (img.top < canvas.top && img.bottom < canvas.bottom) {
4554 img.top = canvas.bottom - img.height;
4555 }
4556 } else {
4557 if (img.top < canvas.top) {
4558 img.top = canvas.top;
4559 }
4560 if (img.bottom > canvas.bottom) {
4561 img.top = canvas.bottom - img.height;
4562 }
4563 }
4564 onPositionChange();
4565 clrscr();
4566 ctx.drawImage(img, img.left, img.top);
4567 }
4568
4569 function imgWheel(e) {
4570 if (!imageField.classList.contains('focused')) {
4571 return;
4572 }
4573 e.preventDefault();
4574
4575 if (img.freeRotationMode) { //Свободное вращение
4576 if (e.deltaY > 0) {
4577 img.rotation < 2 ? img.rotation += imageViewerRotationStep : img.rotation = imageViewerRotationStep;
4578 } else {
4579 img.rotation > 0 ? img.rotation -= imageViewerRotationStep : img.rotation = 2 - imageViewerRotationStep;
4580 }
4581 rotatePic();
4582 } else if (img.freeHueMode) { //Свободный оттенок
4583 if (e.deltaY < 0) {
4584 img.filters.hue <= 358 ? img.filters.hue += 2 : img.filters.hue = 2;
4585 } else {
4586 img.filters.hue >= 2 ? img.filters.hue -= 2 : img.filters.hue = 358;
4587 }
4588 imgLog("Оттенок повёрнут на ${img.filters.hue}°");
4589 applyFilters();
4590 } else if (img.freeContrastMode) { //Свободная контрастность
4591 if (e.deltaY < 0) {
4592 img.filters.contrast <= 199 ? img.filters.contrast += 1 : img.filters.contrast = 200;
4593 } else {
4594 img.filters.contrast >= 1 ? img.filters.contrast -= 1 : img.filters.contrast = 0;
4595 }
4596 imgLog("Контрастность ${img.filters.contrast}%");
4597 applyFilters();
4598 } else if (img.freeBrightnessMode) { //Свободная яркость
4599 if (e.deltaY < 0) {
4600 img.filters.brightness <= 199 ? img.filters.brightness += 1 : img.filters.brightness = 200;
4601 } else {
4602 img.filters.brightness >= 1 ? img.filters.brightness -= 1 : img.filters.brightness = 0;
4603 }
4604 imgLog("Яркость ${img.filters.brightness}%");
4605 applyFilters();
4606 } else if (img.freeSaturationMode) { //Свободная насыщенность
4607 if (e.deltaY < 0) {
4608 img.filters.saturation <= 199 ? img.filters.saturation += 1 : img.filters.saturation = 200;
4609 } else {
4610 img.filters.saturation >= 1 ? img.filters.saturation -= 1 : img.filters.saturation = 0;
4611 }
4612 imgLog("Насыщенность ${img.filters.saturation}%");
4613 applyFilters();
4614 } else {
4615 const oldScale = canvas.scale;
4616 if (e.deltaY > 0) {
4617 canvas.scale -= imageViewerScalingStep + canvas.scale / 10;
4618 if (canvas.scale < canvas.minScale) {
4619 canvas.scale = canvas.minScale;
4620 }
4621 } else {
4622 canvas.scale += imageViewerScalingStep + canvas.scale / 10;
4623 if (canvas.scale > 4) {
4624 canvas.scale = 4;
4625 }
4626 }
4627 onScaleChange();
4628 positionCorrector();
4629 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4630 clrscr();
4631 ctx.drawImage(img, img.left, img.top);
4632 }
4633 }
4634
4635
4636 leftButtsJSON = {
4637 type: "actionButtonGroup",
4638 class: "imageFieldRightControls",
4639 flags: ["unfocusable"],
4640 buttons: [
4641 {
4642 iconClass: "expandButtIcon",
4643 hint: "Развернуть на весь экран.\nДвойной клик по изображению делает то же самое."
4644 },
4645 {
4646 iconClass: "undoButtIcon",
4647 hint: "Повернуть изображение против часовой стрелки."
4648 },
4649 {
4650 iconClass: "redoButtIcon",
4651 hint: "Повернуть изображение по часовой стрелке."
4652 },
4653 {
4654 iconClass: "syncButtIcon",
4655 hint: "Режим свободного вращения колёсиком.\nДвойная активация сбрасывает поворот."
4656 }
4657 ]
4658 };
4659
4660 if (!imageField.classList.contains('noFilters')) {
4661 leftButtsJSON.buttons.push(
4662 {
4663 iconClass: "adjustSolidButtIcon",
4664 hint: "Инвертировать изображение"
4665 },
4666 {
4667 iconClass: "swatchBookIcon",
4668 hint: "Оттенок изображения.\nДвойная активация сбрасывает фильтр."
4669 },
4670 {
4671 iconClass: "adjustButtIcon",
4672 hint: "Контрастность изображения.\nДвойная активация сбрасывает фильтр."
4673 },
4674 {
4675 iconClass: "sunButtIcon",
4676 hint: "Яркость изображения.\nДвойная активация сбрасывает фильтр."
4677 },
4678 {
4679 iconClass: "paletteButtIcon",
4680 hint: "Насыщенность изображения.\nДвойная активация сбрасывает фильтр."
4681 },
4682 {
4683 iconClass: "eraserButtIcon",
4684 hint: "Стереть все фильтры."
4685 }
4686 );
4687 }
4688
4689 leftButts.innerHTML = handleFieldAndGetHTML(leftButtsJSON);
4690 }
4691
4692
4693 const rightButtsJSON = {
4694 type: "linkButtonGroup",
4695 class: "imageFieldRightControls",
4696 flags: ["unfocusable"],
4697 buttons: [
4698 {
4699 link: imageOrigLink ? imageOrigLink : imageSource,
4700 hint: "Открыть изображение в новом окне.",
4701 caption: "Просто ссылка"
4702 }
4703 ]
4704 };
4705
4706 if (!imageField.classList.contains('noSearchButtons')) {
4707 rightButtsJSON.buttons.push(
4708 {
4709 type: "yandexSearchByPic",
4710 hint: "Искать похожие картинки в Яндексе.",
4711 query: imageOrigLink ? imageOrigLink : imageSource
4712 }
4713 );
4714 rightButtsJSON.buttons.push(
4715 {
4716 type: "googleSearchByPic",
4717 hint: "Искать похожие картинки в другом поисковике.",
4718 query: imageOrigLink ? imageOrigLink : imageSource
4719 }
4720 );
4721 }
4722
4723 rightButts.innerHTML = handleFieldAndGetHTML(rightButtsJSON);
4724};
4725
4726window.isInteger = (s) => {
4727 return /^\d+$/.test(s);
4728};
4729
4730window.isFloat = (s) => {
4731 return /^\d+(\.\d+)?$/.test(s);
4732};
4733
4734window.isEmail = (s) => {
4735 return /^([a-z\u00a1-\uffff0-9_\-.])+@([a-z\u00a1-\uffff0-9_\-.])+\.([a-z\u00a1-\uffff]{2,6})$/i.test(s);
4736};
4737
4738window.isIP = (s) => {
4739 return /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gm.test(s);
4740};
4741
4742window.isTelNumber = (n) => {
4743 return /^(\s*)?(\+)?([- _():=+]?\d[- _():=+]?){10,14}(\s*)?$/.test(n);
4744};
4745
4746window.isLink = (zelda) => {
4747 return /^(?:(?:https?):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(zelda); //zelda.substr(0,7)=='http://' || zelda.substr(0,8)=='https://';
4748};
4749
4750window.isServiceLink = (zelda, hyrule) => {
4751 switch (hyrule) {
4752 case 'vkontakte':
4753 return /^http(s)?:\/\/(www\.)?vk\.com\/.*$/.test(zelda);
4754 case 'youtube':
4755 return /^http(s)?:\/\/(www\.)?youtu(be)?\.(com)|(be)\/.*$/.test(zelda);
4756 case 'yadisk':
4757 return /^http(s)?:\/\/(www\.)?yadi\.sk\/.*$/.test(zelda);
4758 case 'telegram':
4759 return /^http(s)?:\/\/(www\.)?t\.me\/.*$/.test(zelda);
4760 case 'instagram':
4761 return /^http(s)?:\/\/(www\.)?instagram\.com\/.*$/.test(zelda);
4762 case 'facebook':
4763 return /^http(s)?:\/\/(www\.)?facebook\.com\/.*$/.test(zelda);
4764 case 'wikipedia':
4765 return /^http(s)?:\/\/(www\.)?((ru\.)|(en\.)|(by\.)|(simple\.))?wikipedia\.org\/.*$/.test(zelda);
4766 case 'kinopoisk':
4767 return /^http(s)?:\/\/(www\.)?kinopoisk\.ru\/.*$/.test(zelda);
4768 case 'yamusic':
4769 return /^https:\/\/music.yandex.ru\/album\/\d+\/track\/\d+/.test(zelda);
4770 }
4771};
4772
4773window.isYaDiskLink = (zelda) => {
4774 return zelda.substr(0, 15) === 'http://yadi.sk/' || zelda.substr(0, 16) === 'https://yadi.sk/';
4775};
4776
4777window.isDate = (text, format) => {
4778 switch (format) {
4779 case 'YYYY-MM-DD':
4780 return /^([012]\d{3})-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/.test(text);
4781 case 'DD-MM-YYYY':
4782 return /^(0[1-9]|[12]\d|3[01])-(0[1-9]|1[012])-([012]\d{3})$/.test(text);
4783 case 'MM-DD-YYYY':
4784 return /^(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])-([012]\d{3})$/.test(text);
4785 case 'YYYY.MM.DD':
4786 return /^([012]\d{3})\.(0[1-9]|1[012])\.(0[1-9]|[12]\d|3[01])$/.test(text);
4787 case 'DD.MM.YYYY':
4788 return /^(0[1-9]|[12]\d|3[01])\.(0[1-9]|1[012])\.([012]\d{3})$/.test(text);
4789 case 'MM.DD.YYYY':
4790 return /^(0[1-9]|1[012])\.(0[1-9]|[12]\d|3[01])\.([012]\d{3})$/.test(text);
4791 }
4792 return false;
4793};
4794
4795window.skipProtocolParametersAndOtherStuff = (s) => {
4796 if (!s) {
4797 return s
4798 }
4799 return s.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").replace(/(?:\?.*)?(?:#.*)?$/i, "").replace(/^(youtube\.)(.*)(\/featured|\/about|\/channels|\/community|\/playlists|\/videos)$/i, '$1$2');
4800};
4801
4802window.isOptional = (props) => { //Проверяет, есть ли у поля флаг опциональности (для необязательных полей)
4803 return !!(props && props.flags && props.flags.includes('optional'));
4804};
4805
4806window.isActive = (btn) => {
4807 return btn.classList.contains('active') || btn.dataset.type === 'linkButton';
4808};
4809
4810window.isGroupInfinite = (groupDiv) => {
4811 return groupDiv.classList.contains('infinite');
4812};
4813
4814window.activateButton = (btn) => {
4815 btn.classList.add('active');
4816};
4817
4818window.deactivateButton = (btn) => {
4819 btn.classList.remove('active');
4820};
4821
4822window.invertButton = (btn) => {
4823 btn.classList.remove('clear');
4824 if (isActive(btn)) {
4825 deactivateButton(btn);
4826 } else {
4827 activateButton(btn);
4828 }
4829};
4830
4831window.date2Unix = (date, format) => {
4832 const dateArr = date.split('-');
4833 switch (format) {
4834 case 'YYYY-MM-DD':
4835 return new Date(dateArr[0] + '-' + dateArr[1] + '-' + dateArr[2]).getTime();
4836 case 'DD-MM-YYYY':
4837 return new Date(dateArr[2] + '-' + dateArr[1] + '-' + dateArr[0]).getTime();
4838 case 'MM-DD-YYYY':
4839 return new Date(dateArr[1] + '-' + dateArr[2] + '-' + dateArr[0]).getTime();
4840 case 'YYYY.MM.DD':
4841 return new Date(dateArr[0] + '.' + dateArr[1] + '.' + dateArr[2]).getTime();
4842 case 'DD.MM.YYYY':
4843 return new Date(dateArr[2] + '.' + dateArr[1] + '.' + dateArr[0]).getTime();
4844 case 'MM.DD.YYYY':
4845 return new Date(dateArr[1] + '.' + dateArr[2] + '.' + dateArr[0]).getTime();
4846 }
4847};
4848
4849window.isFirstDateAheadOfSecond = (date1, format1, date2, format2) => {
4850 return date2Unix(date1, format1) > date2Unix(date2, format2);
4851
4852};
4853
4854window.ce = (error, additional) => { //Выводит в консоль ошибку с определённым текстом
4855 console.error((templateErrorsJSON[error] ? templateErrorsJSON[error] : error) + (additional ? ' (' + additional + ')' : ''));
4856};
4857
4858window.cl = () => {
4859 console.log.apply(this, arguments);
4860};
4861
4862window.escapeHTML = (text) => {
4863 const map = {
4864 '&': '&',
4865 '<': '<',
4866 '>': '>',
4867 '"': '"',
4868 "'": '''
4869 };
4870
4871 return text.replace(/[&<>"']/g, function (m) {
4872 return map[m];
4873 });
4874};
4875
4876window.localize = (slug, noConsole) => {
4877 if (!window.l10n) {
4878 if (!noConsole) {console.error('[Вы пытаетесть что-то перевести функцией localize, но window.l10n у вас не определён (например, вызовом Handlebars-хелпера setLocalization).]');}
4879 return;
4880 }
4881 if (!window.l10n[slug]) {
4882 if (!noConsole) {console.error('[В window.l10n нет строк локализации для "' + slug + '"!]');}
4883 return;
4884 }
4885 let localizedString = window.l10n[slug][window.lang];
4886 if (!localizedString && localizedString !== '') {
4887 localizedString = window.l10n[slug].ru;
4888 }
4889 if (!localizedString && localizedString !== '') {
4890 localizedString = window.l10n[slug].en;
4891 }
4892 return localizedString;
4893};
4894
4895window.isDescendant = (parent, child) => {
4896 if (!parent || !child) {
4897 return false;
4898 }
4899 let node = child.parentNode;
4900 while (node != null) {
4901 if (node === parent) {
4902 return true;
4903 }
4904 node = node.parentNode;
4905 }
4906 return false;
4907};
4908
4909window.checkFilesExtensions = (field, extensions) => {
4910 let soWhat = true;
4911 Array.from(field.querySelectorAll('.file__name')).forEach(function (name) {
4912 let result = false;
4913 Array.from(extensions).forEach(function (ext) {
4914 if (new RegExp('\.' + ext + '$', 'i').test(name.innerHTML)) {
4915 result = true;
4916 }
4917 });
4918 if (!result) {
4919 soWhat = false;
4920 name.classList.add('red');
4921 }
4922 });
4923 return soWhat;
4924};
4925
4926window.getInputFieldHTML = (field, propsIndex) => {
4927 let title = '';
4928 if (field.hint) {
4929 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
4930 }
4931 if (field.properties && field.properties.examples) {
4932 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
4933 }
4934
4935 //Подсказка
4936 let help = '';
4937 if (field.titleHelp) {
4938 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
4939 }
4940
4941 return '' +
4942 '<div class="element ' +
4943 flagsToClassName(field.flags) +
4944 ' ' + (field.class ? field.class + ' ' : '') +
4945 '">' +
4946 '<div class="elementTitle">' +
4947 '<div class="stepQuestion">' + (field.title ? field.title + (field.properties && field.properties.format ? ' (в формате ' + field.properties.format + ')' : '') + ' ' : '') + help + '</div>' +
4948 '</div>' +
4949 '<div class="elementBody fieldType_' + field.type + ' fieldName_' + field.name + '">' +
4950 '<input class="clear ' +
4951 (field.flags && field.flags.includes('alien') ? 'alien' : '') +
4952 (title ? 'cursorHelp ' : '') +
4953 '" data-type="' + field.type + '" data-name="' + field.name + '" ' +
4954 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
4955 (propsIndex || propsIndex === 0 ? 'data-props="' + propsIndex + '"' : '') +
4956 ' type="text" ' + (field.placeholder ? 'placeholder="' + field.placeholder + '"' : '') +
4957 ' ' + (field.title && !title ? 'title="' + field.title + '"' : 'data-title=\'' + field.title.replace(/'/g, '\"') + '\'') + ' ' +
4958 (field.value ? 'value="' + field.value + '"' : '') + ' ' +
4959 (field.flags && field.flags.includes('disabled') ? 'disabled' : '') +
4960 (field.flags && field.flags.includes('readOnly') ? 'readonly' : '') + '>' +
4961 (title ? '<div class="popUpTitle">' + title + '</div>' : '') +
4962 '</div>' +
4963 '</div>';
4964};
4965
4966window.getTextareaFieldHTML = (field, propsIndex) => {
4967 let title = '';
4968
4969 if (field.hint) {
4970 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
4971 }
4972
4973 if (field.properties && field.properties.examples) {
4974 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
4975 }
4976
4977 //Подсказка
4978 let help = '';
4979 if (field.titleHelp) {
4980 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
4981 }
4982
4983 return '' +
4984 '<div class="element ' +
4985 flagsToClassName(field.flags) +
4986 (field.flags && field.flags.includes('selected') ? ' toBeClickedAtStart ' : '') +
4987 ' ' + (field.class ? field.class + ' ' : '') +
4988 '">' +
4989 '<div class="elementTitle">' +
4990 '<div class="stepQuestion">' + (field.title ? field.title + (field.properties && field.properties.format ? ' (в формате ' + field.properties.format + ')' : '') + ' ' : '') + help + '</div>' +
4991 '</div>' +
4992 '<div class="elementBody fieldType_' + field.type + ' fieldName_' + field.name + '">' +
4993 '<textarea class="clear ' +
4994 (field.flags && field.flags.includes('alien') ? ' alien ' : '') +
4995 (title ? 'cursorHelp ' : '') +
4996 '" data-type="' + field.type + '" data-name="' + field.name + '" ' +
4997 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
4998 (propsIndex || propsIndex === 0 ? 'data-props="' + propsIndex + '"' : '') +
4999 ' type="text" ' +
5000 (field.placeholder ? 'placeholder="' + field.placeholder + '"' : '') + ' ' +
5001 (field.title && !title ? 'title="' + field.title + '"' : 'data-title=\'' + field.title.replace(/'/g, '\"') + '\'') + ' ' +
5002 (field.flags && field.flags.includes('disabled') ? 'disabled' : '') +
5003 (field.flags && field.flags.includes('readOnly') ? 'readonly' : '') +
5004 '>' + (field.value ? field.value : '') + '</textarea>' +
5005 (title ? '<div class="popUpTitle"' + title + '</div>' : '') +
5006 '</div>' +
5007 '</div>';
5008};
5009
5010window.getSwitchButtonHTML = (field, propsIndex) => {
5011 let includesString = ' includes';
5012
5013 if (propsIndex && fieldsPropsArray[propsIndex].includes) {
5014 Array.from(fieldsPropsArray[propsIndex].includes).forEach(function (name) {
5015 includesString += ' ' + name;
5016 });
5017 } else {
5018 includesString = '';
5019 }
5020
5021 if (field.flags && field.flags.includes('selected')) {
5022 includesString += ' toBeClickedAtStart';
5023 }
5024
5025 let title = '';
5026 if (field.hint) {
5027 title += '<div class="popUpTitle"><div class="buttPath">Подсказка:</div>' + field.hint + '\n</div>';
5028 includesString += ' hasHint';
5029 }
5030
5031 let additional = '';
5032 if (field.help) {
5033 additional += '<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Помощь:</div>' + field.help + '\n</div></div>';
5034 includesString += ' hasHelp';
5035 }
5036
5037 if (!field.caption) {
5038 field.caption = field.name;
5039 }
5040
5041 return '<div class="buttDiv fieldName_' +
5042 field.name +
5043 (field.flags && field.flags.includes('inline') ? ' inline' : '') +
5044 '">' +
5045 '<div class="btn switchButton correct' +
5046 includesString +
5047 flagsToClassName(field.flags) +
5048 (propsIndex && fieldsPropsArray[propsIndex].excludes ? ' excludesSomething' : '') +
5049 '" data-name="' +
5050 field.name +
5051 '" data-type="switchButton"' +
5052 (propsIndex || propsIndex === 0 ? ' data-props="' + propsIndex + '"' : '') +
5053 (field.radiogroup ? ' data-radiogroup="' + field.radiogroup + '"' : '') +
5054 (field.select ? ' data-select="' + field.select + '"' : '') +
5055 (field.checkboxgroup ? ' data-checkboxgroup="' + field.checkboxgroup + '"' : '') +
5056 (field.groupSolutionName ? ' data-groupsolutionname="' + field.groupSolutionName + '"' : '') +
5057 '>' +
5058 "<i class="ffIcon" data-icon="${field.icon ? field.icon : 'done'}"></i>" +
5059 '<a>' + field.caption + '</a>' +
5060 title +
5061 '</div>' +
5062 additional +
5063 '</div>';
5064};
5065
5066window.getLinkButtonHTML = (field, propsIndex) => {
5067 field.type = field.service;
5068 if (!field.caption) {field.caption = '';}
5069 let includesString = 'includes';
5070 if (propsIndex && fieldsPropsArray[propsIndex].includes) {
5071 Array.from(fieldsPropsArray[propsIndex].includes).forEach(function (name) {
5072 includesString += ' ' + name;
5073 });
5074 } else {
5075 includesString = '';
5076 }
5077 if (field.flags && field.flags.includes('selected')) {
5078 includesString += ' toBeClickedAtStart';
5079 }
5080 if (field.flags && field.flags.includes('mustBeClicked')) {
5081 includesString += ' mustBeClicked';
5082 }
5083 let title = '';
5084 if (field.hint) {
5085 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
5086 }
5087 if (field.properties && field.properties.examples) {
5088 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
5089 }
5090
5091 let href, iconClass, caption;
5092 if (!field.type) {
5093 href = field.link;
5094 iconClass = field.iconClass ? field.iconClass : 'defaultButtIcon';
5095 caption = field.caption;
5096 } else if (linkButtonTypes[field.type]) {
5097 href = field.link ? field.link : linkButtonTypes[field.type].link;
5098 iconClass = field.iconClass ? field.iconClass : linkButtonTypes[field.type].iconClass;
5099 caption = field.caption ? field.caption : linkButtonTypes[field.type].caption;
5100 if (field.query) {
5101 href += encodeURIComponent(field.query);
5102 }
5103 if (linkButtonTypes[field.type].linkAfter) {
5104 href += linkButtonTypes[field.type].linkAfter;
5105 } else if (field.linkAfter) {
5106 href += field.linkAfter;
5107 }
5108 } else {
5109 ce('wrong_linkButtonGroup_button_type', 'type:' + field.type + ', caption:' + field.caption);
5110 }
5111
5112 return '<div class="buttDiv linkButton_' + field.type + (field.flags && field.flags.includes('inline') ? ' inline' : '') + '"><div class="btn linkButton ' + iconClass + ' ' + field.class + ' ' + field.name + ' ' + includesString + flagsToClassName(field.flags) + '" data-type="linkButton"' + (propsIndex || propsIndex === 0 ? ' data-props="' + propsIndex + '"' : '') + ' data-name="' + field.name + '"><span class="linkButtonIconContainer"><i></i></span><a ' + (href ? 'target="_blank" class="unclicked" href="' + href + '"' : '') + '>' + caption + '</a>' + (title ? '<div class="popUpTitle">' + title + '</div>' : '') + '</div></div>';
5113};
5114
5115window.getImageViewerHTML = (field, propsIndex) => {
5116 //Подсказка
5117 let help = '';
5118 if (field.titleHelp) {
5119 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
5120 }
5121
5122 let output =
5123 '<div class="element imageField fieldName_' + field.name +
5124 flagsToClassName(field.flags) +
5125 '">' +
5126 (field.title ? '<div class="stepTitle">' + field.title + help +'</div>' : '') +
5127 '<div class="imageHeader">' +
5128 '<div class="imageSource">' + field.link + '</div>' +
5129 (field.origLink ? '<div class="imageOriginalLink">' + field.origLink + '</div>' : '') +
5130 '<div class="imageInfo"></div>' +
5131 '<div class="imageButts"></div>' +
5132 '</div>' +
5133 '<div class="imageBlock">' +
5134 '<canvas></canvas>' +
5135 '<div class="imageConsole"></div>' +
5136 '</div>' +
5137 '<div class="imageFooter">' +
5138 '<div class="imageLeftButts"></div>' +
5139 '<div class="imageRightButts"></div>' +
5140 '</div>' +
5141 '</div>';
5142 return output;
5143};
5144
5145window.getFileFieldHTML = (field, propsIndex) => {
5146 //Подсказка
5147 let help = '';
5148 if (field.titleHelp) {
5149 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
5150 }
5151
5152 let output =
5153 "<div class="element fileField fieldName_${field.name} ${flagsToClassName(field.flags)}" ${field.caption ? "data-caption="${field.caption}"" : ''}
5154 ${propsIndex ? "data-props="${propsIndex}"" : ''}
5155 ${field.name ? "data-name="${field.name}"" : ''}
5156 >
5157 <div class="stepTitle title">${field.title || ''} ${help}</div>
5158 <div class="fContainer"></div>
5159 </div>";
5160 return output;
5161};
5162
5163window.flagsToClassName = (flags) => {
5164 if (!flags || !Array.isArray(flags)) {
5165 return '';
5166 }
5167
5168 function maybeRename(flag) {
5169 switch (flag) {
5170 case 'block':
5171 return '';
5172 case 'oldSchool':
5173 return 'asButtons';
5174 case 'focused':
5175 return 'focusMeOnStart';
5176 default:
5177 return flag;
5178 }
5179 }
5180
5181 let s = ' ';
5182 for (const flag of flags) {
5183 s += maybeRename(flag) + ' ';
5184 }
5185 return s;
5186};
5187
5188//Функция получает на вход поле из входной спеки, обрабатывает его и возвращает его HTML код
5189window.handleFieldAndGetHTML = (field, handlebarsContext) => {
5190 //Если у поля не прописаны обязательные поля - забиваем
5191 if (!field.type) {
5192 return;
5193 }
5194 if (!field.title) {
5195 field.title = '';
5196 }
5197
5198 if (!handlebarsContext) {
5199 if (this && this.getTask) {
5200 handlebarsContext = this.getTask().input_values;
5201 } else {
5202 handlebarsContext = window.handlebarsContext;
5203 }
5204 }
5205
5206 //Проверка полей на операторы, плейсхолдеры и т.д.
5207 for (const p in field) {
5208 if (typeof (field[p]) != "string") {
5209 continue;
5210 }
5211 const split = field[p].split('|||');
5212 if (split[1]) { //Похоже, что поле с оператором
5213 switch (split[0]) {
5214 case 'INPUT': //В сплит3 вместо ▼ надо будет подставить значение входного поля сплит2
5215 const input = window.currentRenderingTask.getTask().input_values;
5216 if (!split[1] || !split[2]) {
5217 ce('output_placeholder_input_insufficient_parameters', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5218 break
5219 }
5220 if (!input[split[1]] || (typeof (input[split[1]]) === "string" && input[split[1]].length === 0)) {
5221 if (field.default && p !== 'default') {
5222 field[p] = field.default;
5223 } else {
5224 return '';
5225 }
5226 } else {
5227 field[p] = split[2].replace('▼', input[split[1]]).replace('▼', input[split[1]]);
5228 }
5229 break;
5230 case 'somethingOther':
5231 break;
5232 default:
5233 ce('output_placeholder_not_found', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5234 break;
5235 }
5236 }
5237 }
5238
5239 //Локализация [v1.18+]
5240 for (const p in field) {
5241 if (typeof field[p] == 'object' && (field[p].ru || field[p].en)) {
5242 field[p] = field[p][window.lang];
5243 }
5244 }
5245
5246 //Компилируем Handlebars в свойствах поля
5247 function compileStringsHere(where) {
5248 for (const p in where) {
5249 if (!where[p]) {
5250 //continue;
5251 } else
5252 if (typeof where[p] === 'string') {
5253 where[p] = Handlebars.compile(where[p])(handlebarsContext);
5254 } else if (typeof where[p] === 'object'){
5255 compileStringsHere(where[p]);
5256 }
5257 /*if (Array.isArray(where[p])) {
5258 for (const e in where[p]) {
5259 where[p][e] = Handlebars.compile(where[p][e])(handlebarsContext);
5260 }
5261 } else
5262 if (where[p] instanceof Object) {
5263 compileStringsHere(where[p]);
5264 }*/
5265 }
5266 }
5267 if (handlebarsContext) {
5268 compileStringsHere(field);
5269 }
5270
5271 //Если у поля есть массив properties - нам надо добавить его в fieldsPropsArray и в dataset поля добавить ссылку, т.к. потом, очевидно, пригодятся
5272 let propsIndex = null;
5273 if (field.properties) {
5274 propsIndex = fieldsPropsArray.length;
5275 fieldsPropsArray.push(field.properties);
5276 }
5277
5278
5279 //Определимся с типом поля, т.к. от этого будут зависеть наши дальнейшие действия
5280 let groupHTML, help;
5281 switch (field.type) {
5282 case 'date': //Поле со вводом даты. Обычный инпут.
5283 case 'text': //Поле со вводом текста. Обычный инпут.
5284 case 'email': //Поле со вводом мыла. Обычный инпут.
5285 case 'link': //Поле со вводом ссылки. Обычный инпут.
5286 case 'integer': //Поле со вводом целого числа. Обычный инпут.
5287 case 'float': //Поле со вводом дробного числа. Обычный инпут.
5288 return getInputFieldHTML(field, propsIndex);
5289 case 'textarea':
5290 return getTextareaFieldHTML(field, propsIndex);
5291 case 'switchButton':
5292 return getSwitchButtonHTML(field, propsIndex);
5293 case 'linkButton':
5294 if (field.fieldType === 'linkButtonGroup' && !field.service && !field.caption) {
5295 return '';
5296 } //Если надо вывести кнопки только для определённых языков, например
5297 return getLinkButtonHTML(field, propsIndex);
5298 case 'image':
5299 return getImageViewerHTML(field, propsIndex);
5300 case 'file':
5301 return getFileFieldHTML(field, propsIndex);
5302 case 'title':
5303 return '<div class="element unfocusable ' + (field.class ? field.class : '') + '"><div class="elementTitle"><div class="title">' + field.caption + '</div></div></div>';
5304 case 'title2':
5305 return '<div class="element unfocusable ' + (field.class ? field.class : '') + '"><div class="elementTitle"><div class="stepQuestion title">' + field.caption + '</div></div></div>';
5306 case 'label':
5307 return '<div class="element unfocusable ' + (field.class ? field.class : '') + '">' + (field.title ? '<div class="elementTitle"><div class="stepTitle labelTitle">' + field.title + '</div></div>' : '') + '<div class="elementBody"><div class="label caption ' + (field.name ? field.name : '') + '">' + field.caption + '</div></div></div>';
5308 case 'clickableLink':
5309 if (!field.caption) {
5310 field.caption = decodeURIComponent(field.link.replace(/\+/g, ' '));
5311 }
5312 return '<div class="element unfocusable">' + (field.title ? '<div class="elementTitle"><div class="stepTitle clickableLinkTitle">' + field.title + ':</div></div>' : '') + '<div class="elementBody"><div class="link ' + (field.name ? field.name : '') + '"><a href="' + field.link + '" target="_blank"><i class="material-icons">open_in_new</i>' + field.caption + '</a></div></div></div>';
5313 case 'radioGroup':
5314 case 'checkboxGroup':
5315 case 'buttonGroup':
5316 //Подсказка
5317 help = '';
5318 if (field.titleHelp) {
5319 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
5320 }
5321
5322 groupHTML = field.title ? '<div class="stepTitle title">' + field.title + help + '</div>' : '';
5323 Array.from(field.buttons).forEach(function (button) {
5324 if (!field.name && field.type !== 'buttonGroup') {
5325 ce('no_field_name', 'type:' + field.type + ', title:' + field.title);
5326 return;
5327 }
5328
5329 if (!button.name) {
5330 ce('no_button_name', 'field type:' + field.type + ', field title:' + field.title + ', button caption:' + field.caption);
5331 return;
5332 }
5333
5334 button.type = 'switchButton';
5335 button[field.type.toLowerCase()] = field.name;
5336 if (field.solutionName) {button['groupSolutionName'] = field.solutionName;}
5337
5338 if (!button.flags) {
5339 button.flags = [];
5340 }
5341
5342 if (field.flags && field.flags.includes('block')) {
5343 button.flags.push('block');
5344 }
5345
5346 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5347 button.flags.push('inline');
5348 }
5349
5350 groupHTML += handleFieldAndGetHTML(button);
5351 });
5352
5353 if (!field.title) {
5354 field.title = '';
5355 }
5356
5357 return "<div class="${field.type} element fieldName_${field.name} ${field.name} ${flagsToClassName(field.flags)}"
5358 data-name="${field.name}"
5359 ${field.solutionName ? "data-solutionname="${field.solutionName}"" : ''}
5360 data-title='${field.title.replace(/'/g, '\"')}'
5361 >${groupHTML}</div>";
5362 case 'select':
5363 groupHTML = '';
5364 Array.from(field.buttons).forEach(function (button) {
5365 if (!field.name && field.type !== 'buttonGroup') {
5366 ce('no_field_name', 'type:' + field.type + ', title:' + field.title);
5367 return;
5368 }
5369 if (!button.name) {
5370 ce('no_button_name', 'field type:' + field.type + ', field title:' + field.title + ', button caption:' + field.caption);
5371 return;
5372 }
5373 button.type = 'switchButton';
5374 button[field.type.toLowerCase()] = field.name;
5375 if (field.solutionName) {button['groupSolutionName'] = field.solutionName;}
5376 if (!button.flags) {
5377 button.flags = [];
5378 }
5379 if (field.flags && field.flags.includes('block')) {
5380 button.flags.push('block');
5381 }
5382 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5383 button.flags.push('inline');
5384 }
5385 groupHTML += handleFieldAndGetHTML(button);
5386 });
5387 if (!field.title) {
5388 field.title = '';
5389 }
5390
5391 let includesString, title = '';
5392 if (field.hint) {
5393 title += '<div class="popUpTitle"><div class="buttPath">Подсказка:</div>' + field.hint + '\n</div>';
5394 includesString += ' hasHint';
5395 }
5396
5397 let additional = '';
5398 if (field.help) {
5399 additional += '<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Помощь:</div>' + field.help + '\n</div></div>';
5400 includesString += ' hasHelp';
5401 }
5402
5403 return '<div class="' + field.type + ' element fieldName_' + field.name + ' ' + field.name + ' ' +
5404 flagsToClassName(field.flags) + includesString +
5405 '" style="' +
5406 (field.width ? " width: ${field.width};" : '') +
5407 '" data-name="' + field.name + '"' +
5408 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
5409 ' data-title=\'' + field.title.replace(/'/g, '\"') + '\'>' +
5410 (field.title ? '<div class="stepTitle title">' + field.title + additional + '</div>' : '') +
5411 '<div class="selectHead">' +
5412 '<div class="selectDisplay">' +
5413 '<div class="selectText">' +
5414 '</div>' +
5415 '</div>' +
5416 '<div class="selectButtons">' +
5417 '<i class="ffIcon empty" data-icon="times"></i>' +
5418 '<i class="ffIcon arrow" data-icon="angle_down"></i>' +
5419 '</div>' + title +
5420 '</div>' +
5421 '<div class="selectContainer">' +
5422 groupHTML +
5423 '</div>'+
5424 '</div>';
5425 case 'group':
5426 //Генерируем HTML всей внутрянки этой группы
5427 groupHTML = '';
5428 Array.from(field.groupFields).forEach(function (groupField) {
5429 groupHTML += handleFieldAndGetHTML(groupField);
5430 });
5431 //Записываем её в наш массив свойств
5432 propsIndex = fieldsPropsArray.length;
5433 fieldsPropsArray.push(groupHTML);
5434 //Возвращаем обёртку группы со всеми нужными параметрами. Внутрянку будем добавлять/удалять по мере необходимости
5435 return "<div class="groupContainer fieldName_${field.name} " +
5436 field.name +
5437 (field.flags && field.flags.includes('excluded') ? ' excluded' : '') +
5438 (field.flags && field.flags.includes('inline') ? ' inline' : '') +
5439 (field.flags && field.flags.includes('infinite') ? ' infinite' : '') +
5440 "" data-name="${field.name}" data-codepropindex="${propsIndex}"" +
5441 (field.limit ? " data-limit="${field.limit}"" : '') +
5442 (field.flags && field.flags.includes('excluded') ? ' data-excluded="excluded"' : '') +
5443 '></div>';
5444 case 'actionButtonGroup':
5445 case 'linkButtonGroup':
5446 //Подсказка
5447 help = '';
5448 if (field.titleHelp) {
5449 help = "<div class="helpContainer"><i class="ffIcon" data-icon="help"></i><div class="popUpTitle" style="height: 0;"><div class="buttPath">Подсказка:</div>${field.titleHelp}</div></div>";
5450 }
5451
5452 groupHTML = field.title ? '<div class="stepTitle title">' + field.title + help + '</div>' : '';
5453
5454 if (!field.title) {
5455 field.title = '';
5456 }
5457
5458 if (!field.name) {
5459 field.name = '';
5460 }
5461
5462 if (!field.class) {
5463 field.class = field.name;
5464 }
5465
5466 Array.from(field.buttons).forEach(function (button) {
5467 button.service = button.type;
5468 button.type = 'linkButton';
5469 button.fieldType = field.type;
5470 button[field.type.toLowerCase()] = field.class;
5471 if (field.type === 'actionButtonGroup') {
5472 button.link = '';
5473 if (!button.iconClass) {
5474 button.iconClass = 'actionButtonNoIcon';
5475 }
5476 }
5477 if (!button.flags) {
5478 button.flags = [];
5479 }
5480 if (field.flags && field.flags.includes("block")) {
5481 button.flags.push("block");
5482 }
5483 if (!button.name) {
5484 button.name = field.name;
5485 }
5486 if (!button.class) {
5487 button.class = field.class;
5488 }
5489 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5490 button.flags.push('inline');
5491 }
5492 groupHTML += handleFieldAndGetHTML(button);
5493 });
5494 return '<div class="' + field.type + ' element ' + field.name + ' alien' +
5495 (field.flags && field.flags.includes('focused') ? ' focusMeOnStart' : '') +
5496 (field.flags && field.flags.includes('unfocusable') ? ' unfocusable ' : ' ') +
5497 field.class + '" data-name="' + field.name + '" data-title=\'' + field.title.replace(/'/g, '\"') + '\'>' + groupHTML + '</div>';
5498 case 'view':
5499 return Handlebars.compile("
5500 {{#v name="${field.name}" class="${field.class ? field.class : ''} ${field.flags && field.flags.includes('hidden') ? 'hidden' : ''}"}}
5501 ${field.content}
5502 {{/v}}
5503 ")(handlebarsContext);
5504 case 'null':
5505 return '';
5506 }
5507 ce('wrong_field_type', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5508};
5509
5510window.getFieldsHTML = (fields, context) => {
5511 let fieldsName = '';
5512 if (!context || !context[fields]) {
5513 if (this && this.getTask) {
5514 context = this.getTask().input_values;
5515 } else {
5516 context = window.handlebarsContext;
5517 }
5518 }
5519 if (fields && typeof fields === "string") {
5520 fieldsName = fields;
5521 fields = context[fieldsName];
5522 }
5523 if (!fields || !Array.isArray(fields)) {
5524 console.log(context);
5525 ce('Что-то неправильное передано хелперу drawFields: ', [fieldsName, fields ? fields : '[пустота]']);
5526 return;
5527 }
5528 let out = '';
5529 Array.from(fields).forEach(function (field) {
5530 out += handleFieldAndGetHTML.apply(this, [field, context]);
5531 });
5532 if (fieldsName) {
5533 out = "<div class="theOneFieldsContainer fieldsName_${fieldsName}" data-name="${fieldsName}">${out}</div>";
5534 }
5535 return out;
5536};
5537
5538
5539
5540
5541
5542
5543/* Всякие служебные переменные и JSONки */
5544
5545window.specObjectNameOfTheField = [];
5546window.fieldsPropsArray = [];
5547
5548//Настройки шаблона, работающие через CSS (если какая-то из них false, то в класс сьюта добавится "no_[имя настройки]", например "no_taskList")
5549window.cssNoClassSettings = ['taskSwitcher', 'taskCounter', 'taskList', 'taskMinimizer', 'notForDirect'];
5550window.cssClassSettings = ['fullScreenMode', 'verticalSuite'];
5551
5552//СВГшные иконки, доступные к использованию с классом ffIcon (в data-icon)
5553window.svgIcons = {
5554 "keyboard_arrow_up": {
5555 "attributes": 'viewBox="0 0 320 512"',
5556 "path": "M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
5557 },
5558 "keyboard_tab": {
5559 "attributes": 'viewBox="0 0 448 512"',
5560 "path": "M448 88v336c0 13.3-10.7 24-24 24h-24c-13.3 0-24-10.7-24-24V88c0-13.3 10.7-24 24-24h24c13.3 0 24 10.7 24 24zm-280.5 66.4l65.6 65.6H24c-13.3 0-24 10.7-24 24v24c0 13.3 10.7 24 24 24h209.1l-65.6 65.6c-9.4 9.4-9.4 24.6 0 33.9l17 17c9.4 9.4 24.6 9.4 33.9 0L353.9 273c9.4-9.4 9.4-24.6 0-33.9L218.4 103.5c-9.4-9.4-24.6-9.4-33.9 0l-17 17c-9.4 9.4-9.4 24.6 0 33.9z"
5561 },
5562 "keyboard_backspace": {
5563 "attributes": 'viewBox="0 0 448 512"',
5564 "path": "M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z"
5565 },
5566 "invert_colors": {
5567 "attributes": 'viewBox="0 0 512 512"',
5568 "path": "M8 256c0 136.966 111.033 248 248 248s248-111.034 248-248S392.966 8 256 8 8 119.033 8 256zm248 184V72c101.705 0 184 82.311 184 184 0 101.705-82.311 184-184 184z"
5569 },
5570 "open_in_new": {
5571 "attributes": 'viewBox="0 0 576 512"',
5572 "path": "M448 279.196V464c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h248a24 24 0 0 1 16.97 7.029l16 16C344.09 102.149 333.382 128 312 128H64v320h320V295.196c0-6.365 2.529-12.47 7.029-16.971l16-16C422.148 247.106 448 257.814 448 279.196zM576 37.333C576 16.715 559.285 0 538.667 0H380c-15.464 0-28 12.536-28 28v17.885c0 15.766 13.011 28.424 28.772 27.989l67.203-1.906L199.09 319.09c-9.429 9.363-9.457 24.605-.061 34.001l23.879 23.879c9.396 9.396 24.639 9.369 34.001-.06l247.122-248.885-1.906 67.203c-.434 15.76 12.224 28.772 27.99 28.772H548c15.464 0 28-12.536 28-28V37.333z"
5573 },
5574 "done": {
5575 "attributes": 'viewBox="0 0 512 512"',
5576 "path": "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"
5577 },
5578 "angle_down": {
5579 "attributes": 'viewBox="0 0 320 512"',
5580 "path": "M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z"
5581 },
5582 "times": {
5583 "attributes": 'viewBox="0 0 352 512"',
5584 "path": "M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
5585 },
5586 "eye": {
5587 "attributes": 'viewBox="0 0 576 512"',
5588 "path": "M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"
5589 },
5590 "comment_smile": {
5591 "attributes": 'viewBox="0 0 512 512"',
5592 "path": "M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32zm64 133.2c14.8 0 26.8 12 26.8 26.8s-12 26.8-26.8 26.8-26.8-12-26.8-26.8 12-26.8 26.8-26.8zm-128 0c14.8 0 26.8 12 26.8 26.8s-12 26.8-26.8 26.8-26.8-12-26.8-26.8 12-26.8 26.8-26.8zm164.2 140.9C331.3 335.3 294.8 352 256 352c-38.8 0-75.3-16.7-100.2-45.9-5.8-6.7-5-16.8 1.8-22.5 6.7-5.7 16.8-5 22.5 1.8 18.8 22 46.5 34.6 75.8 34.6 29.4 0 57-12.6 75.8-34.7 5.8-6.7 15.9-7.5 22.6-1.8 6.8 5.8 7.6 15.9 1.9 22.6z"
5593 },
5594 "help": {
5595 "attributes": 'viewBox="0 0 448 512"',
5596 "path": "M224 202a42 42 0 1 0-42-42 42 42 0 0 0 42 42zm44 134h-12V236a12 12 0 0 0-12-12h-64a12 12 0 0 0-12 12v24a12 12 0 0 0 12 12h12v64h-12a12 12 0 0 0-12 12v24a12 12 0 0 0 12 12h88a12 12 0 0 0 12-12v-24a12 12 0 0 0-12-12z",
5597 "translucentPath": "M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48zm-176 86a42 42 0 1 1-42 42 42 42 0 0 1 42-42zm56 254a12 12 0 0 1-12 12h-88a12 12 0 0 1-12-12v-24a12 12 0 0 1 12-12h12v-64h-12a12 12 0 0 1-12-12v-24a12 12 0 0 1 12-12h64a12 12 0 0 1 12 12v100h12a12 12 0 0 1 12 12z"
5598 },
5599 "helpRound": {
5600 "attributes": 'viewBox="0 0 512 512"',
5601 "path": "M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z"
5602 }
5603};
5604
5605//Возможные выводимые в консоль ошибки в шаблоне, для упрощения отладки и устранения проблем со входным JSON
5606window.templateErrorsJSON = {
5607 "output_placeholder_not_found":
5608 'Судя по всему, вы попытались использовать операторы в одном из полей JSONки - найдено сочетание "|||" Но вот имя оператора мне не понятно. Если вы ничего такого делать не собирались - пожалуйста, не используйте "|||" в JSONке.',
5609 "output_placeholder_input_insufficient_parameters":
5610 'Недостаточно параметров у оператора в JSON. У оператора INPUT три параметра - сам оператора, имя входного поля и текст, в который вместо плейсхолдера "▼" (Alt+31) будет подставлено значение этого поля.',
5611 "wrong_field_type":
5612 'Неверный тип у поля, вместо этого выведено "undefined"',
5613 "wrong_linkButtonGroup_button_type":
5614 'Неверный тип у кнопки в linkButtonGroup, она не выведена.',
5615 "no_field_name":
5616 'У поля отсутствует свойство name!',
5617 "no_button_name":
5618 'У кнопки отсутствует свойство name!',
5619 "valuesToWrite_is_not_an_array":
5620 'Свойство valuesToWrite не является массивом объектов!',
5621 "valuesToWrite_has_no_selector":
5622 'У одного из объектов массива valuesToWrite отсутствует свойство "selector". Укажите его, чтобы было понятно, в свойство property элемента по какому селектору вставлять значение value.',
5623 "valuesToWrite_has_no_property":
5624 'У одного из объектов массива valuesToWrite отсутствует свойство "property". Укажите его, чтобы было понятно, какое свойство заменять у указанного элемента с селектором selector (например, "value", если это input или textarea или "innerHTML" если это div или span).',
5625 "valuesToWrite_has_no_value":
5626 'У одного из объектов массива valuesToWrite отсутствует свойство "value". Укажите его, чтобы было понятно, какое именно значение вставлять в свойство property элемента с селектором selector.',
5627 "valuesToWrite_target_not_found":
5628 'Только что нажатая кнопка должна была прописать значение в свойство элемента по определённому селектору (свойство valuesToWrite во входном JSON), но нужный элемент по указанному селектору найти так и не удалось... ',
5629 "specmapper_conversion_type_not_found":
5630 'Указанного типа конверсии в правиле specMapper"а не существует.',
5631 "specmapper_conversion_no_function":
5632 'В specMapper"е указан тип конверсии - функция, но не указана сама функция.'
5633};
5634
5635//Возможные типы linkButton и их параметры
5636window.linkButtonTypes = {
5637 "yandexSearch": {
5638 iconClass: "yandexButtIcon",
5639 link: "https://yandex.ru/yandsearch?text=",
5640 caption: "Яндекс.Поиск"
5641 },
5642 "yandexPics": {
5643 iconClass: "yandexButtIcon",
5644 link: "https://yandex.ru/images/search?text=",
5645 caption: "Яндекс.Картинки"
5646 },
5647 "yandexSearchByPic": {
5648 iconClass: "yandexButtIcon",
5649 link: "https://yandex.by/images/search?url=",
5650 "linkAfter": "&rpt=imageview",
5651 caption: "Поиск по картинке"
5652 },
5653 "yandexNews": {
5654 iconClass: "yandexButtIcon",
5655 link: "https://news.yandex.ru/yandsearch?rpt=nnews2&grhow=clutop&text=",
5656 caption: "Яндекс.Новости"
5657 },
5658 "yandexVideo": {
5659 iconClass: "yandexButtIcon",
5660 link: "https://yandex.ru/video/search?text=",
5661 caption: "Яндекс.Видео"
5662 },
5663 "yandexTranslate": {
5664 iconClass: "yandexButtIcon",
5665 link: "https://translate.yandex.ru/?text=",
5666 caption: "Яндекс.Переводчик"
5667 },
5668 "googleSearch": {
5669 iconClass: "googleButtIcon",
5670 link: "https://www.google.ru/search?q=",
5671 caption: "Google.Поиск"
5672 },
5673 "googlePics": {
5674 iconClass: "googleButtIcon",
5675 link: "https://www.google.ru/search?&hl=ru&site=imghp&tbm=isch&source=hp&biw=1467&bih=682&q=",
5676 caption: "Google.Картинки"
5677 },
5678 "googleSearchByPic": {
5679 iconClass: "googleButtIcon",
5680 link: "https://www.google.by/searchbyimage?image_url=",
5681 caption: "Поиск по картинке"
5682 },
5683 "googleNews": {
5684 iconClass: "googleButtIcon",
5685 link: "https://www.google.ru/search?hl=ru&gl=ru&tbm=nws&authuser=0&q=",
5686 caption: "Google.Новости"
5687 },
5688 "googleVideo": {
5689 iconClass: "googleButtIcon",
5690 link: "https://www.google.ru/search?tbm=vid&hl=ru&source=hp&biw=&bih=&q=",
5691 caption: "Google.Видео"
5692 },
5693 "googleTranslate": {
5694 iconClass: "googleButtIcon",
5695 link: "https://translate.google.ru/#auto/ru/",
5696 caption: "Google.Переводчик"
5697 },
5698 //Украина
5699 "yandexSearchUA": {
5700 iconClass: "yandexButtIcon",
5701 link: "https://yandex.ua/search/?lr=143&msid=1464598804.64254.22885.27565&text=",
5702 caption: "Яндекс.Поиск (укр)"
5703 },
5704 "yandexVideoUA": {
5705 iconClass: "yandexButtIcon",
5706 link: "https://yandex.ua/video/search?text=",
5707 caption: "Яндекс.Видео (укр)"
5708 },
5709 "googleSearchUA": {
5710 iconClass: "googleButtIcon",
5711 link: "https://www.google.com.ua/#q=",
5712 caption: "Google.Поиск (укр)"
5713 },
5714 "googleVideoUA": {
5715 iconClass: "googleButtIcon",
5716 link: "https://www.google.com.ua/search?tbm=vid&hl=ru-UA&source=hp&biw=&bih=&q=",
5717 "linkAfter": "&tbm=vid",
5718 caption: "Google.Видео (укр)"
5719 }
5720};
5721
5722window.theOneCSS = "
5723body {
5724 overflow-x: hidden;
5725 font-size: 14px;
5726}
5727.displayNone {
5728 display: none !important;
5729}
5730.block {
5731 font-family: Helvetica,Arial,sans-serif;
5732 border: 0;
5733 border-radius: 4px;
5734 padding: 20px 10px 10px 10px;
5735 margin: 10px auto 35px;
5736 position: relative;
5737 background: #fff;
5738 max-width: 1200px;
5739 box-shadow: 5px 5px 18px #0001;
5740 text-align: left;
5741}
5742.task_focused .block {
5743 border: 1px solid #81c45600;
5744 box-shadow: 5px 5px 18px #0002;
5745}
5746.blockTitle {
5747 position: absolute;
5748 top: -15px;
5749 background: #e6e6e6;
5750 display: inline-block;
5751 padding: 8px 10px 6px 10px;
5752 font-size: 14px;
5753 line-height: 14px;
5754 border-radius: 4px;
5755 font-weight: 600;
5756 transition: all .2s ease-in 0s;
5757 margin-left: 8px;
5758}
5759.blockButtons {
5760 position: absolute;
5761 display: flex;
5762 top: -15px;
5763 right: 18px;
5764 transition: all .2s ease-in 0s;
5765}
5766.blockButtons .blockTitle {
5767 position: relative;
5768 display: block;
5769 width: fit-content;
5770 margin-left: 18px;
5771 padding: 0px;
5772 height: 28px;
5773 top: initial;
5774 left: initial;
5775 right: initial;
5776}
5777.task_focused .blockTitle {
5778 opacity: 1;
5779}
5780/*.deskTitle {
5781 font-weight: 600;
5782}*/
5783.buttDiv {
5784 margin-right: 10px;
5785}
5786.buttDiv:last-child {
5787 margin-right: 0;
5788}
5789.btn {
5790 margin-left: 9px;
5791 margin-bottom: 5px;
5792 display: inline-block;
5793 transition: max-height .3s ease-in, max-width .3s ease-in, opacity .2s ease-in;
5794 max-height: 50px;
5795 opacity: 1;
5796 user-select: none;
5797 cursor: pointer;
5798 position: relative;
5799 -moz-user-select: none;
5800}
5801.element .btn {
5802 margin-left: 0;
5803}
5804.btn a {
5805 font-size: 15px;
5806 color: #404040;
5807 display: inline-block;
5808 padding: 5px 12px 6px;
5809 background: #FFFBEA;
5810 margin: 4px 0 0;
5811 border-radius: 3px;
5812 border: 1px solid #d9d9d9;
5813 cursor: pointer;
5814 font-weight: 400;
5815 white-space: normal;
5816 position: relative;
5817 min-width: 16px;
5818 transition: all .3s ease-in-out 0s, box-shadow .1s ease-in 0s;
5819 text-align: center;
5820 max-width: calc(100vw - 80px);
5821 overflow: hidden;
5822}
5823.btn.hasHint a, .btn.hasHelp a {
5824 cursor: help;
5825}
5826.element:not(.asButtons) .btn a {
5827 text-align: left;
5828}
5829.btn.underPressure a {
5830 box-shadow: 0 0 1px 0 #0002 !important;
5831}
5832.btn.underPressure {
5833 left: 1px;
5834 top: 1px;
5835}
5836.btn.underPressure .popUpTitle {
5837 top: 39px;
5838 left: -1px;
5839}
5840.btn.noIcon a {
5841 padding-left: 13px;
5842}
5843.btn:hover a {
5844 box-shadow: 0 0 8px 1px #ffe478;
5845 z-index: 1;
5846}
5847.btn>i {
5848 position: absolute;
5849 left: 3px;
5850 font-size: 22px;
5851 top: 0px;
5852}
5853/*@media (max-width: 900px) {
5854 .btn a {
5855 font-size: 14px;
5856 padding: 2px 6px;
5857 font-weight: 400;
5858 }
5859 .darkSide .btn a {
5860 font-weight: 100;
5861 }
5862 .btn:not(.linkButton) i {
5863 font-size: 19px;
5864 top: 5px;
5865 }
5866}*/
5867table .btn a {
5868 padding: 3px 15px;
5869 margin-bottom: 8px;
5870}
5871.btn.active a {
5872 background: #ffe478;
5873}
5874.selected .btn, .filtered .btn.filterMe {
5875 max-height: 0;
5876 max-width: 0;
5877 margin: 0;
5878 opacity: 0;
5879}
5880.selected .buttons .btn.active {
5881 max-height: 50px;
5882 max-width: 400px;
5883 opacity: 1;
5884}
5885.selected .buttons {
5886 flex-wrap:nowrap;
5887}
5888.group {
5889 border-radius: 5px;
5890}
5891/*.stepNumber {
5892 z-index: -1;
5893 position: absolute;
5894 color: #ffe4789e;
5895 font-weight: 900;
5896 font-size: 55px;
5897 right: 2px;
5898 top: 0;
5899 opacity: .6;
5900 font-family: Helvetica, sans-serif;
5901 height: 141px;
5902 line-height: 94px;
5903}*/
5904.answerBlock {
5905 position: relative;
5906}
5907.stepQuestion {
5908 font-size: 14px;
5909 font-weight: 400;
5910 white-space: pre-wrap;
5911}
5912.stepTitle {
5913 font-weight: 600;
5914 margin-bottom: 5px;
5915}
5916.step {
5917 z-index: 9;
5918 position: relative;
5919 max-height: 0px;
5920 overflow: hidden;
5921 transition: all 0.5s cubic-bezier(0.6, 0, 0.4, 1);
5922}
5923.open {
5924 max-height: 2000px;
5925 overflow: visible;
5926}
5927/*.commentBlock .field_type_textarea {
5928 width: 100% !important;
5929 max-width: 500px;
5930}
5931.commentHint {
5932 font-size: 14px;
5933}
5934.field.field_type_checkbox {
5935 position: absolute;
5936}
5937.field.field_type_checkbox label {
5938 display: none;
5939}*/
5940.btn.btnDarkSide a {
5941 box-shadow: 5px 5px 18px #0002;
5942 margin: 0 20px 20px;
5943 padding: 4px;
5944 line-height: 12px;
5945 width: 26px;
5946 height: 26px;
5947 cursor: pointer;
5948}
5949.btn.btnDarkSide a i {
5950 top: 6px;
5951 left: 6px;
5952}
5953.btn.btnDarkSide {
5954 width: 100%;
5955 text-align: center;
5956 display: none;
5957 max-width: initial;
5958 margin: 0 auto;
5959 max-height: 100px;
5960}
5961.btn[name=result]>i {
5962 left: 0.45em;
5963}
5964.task-suite .task:first-child .btnDarkSide {
5965 display: block;
5966}
5967.darkButt {
5968 padding: 0 2px;
5969}
5970.blockButtons .blockTitle:not(.custom) .ffIcon {
5971 opacity: 0.6;
5972 cursor: pointer;
5973 transition: .3s ease-out;
5974 font-size: 28px;
5975}
5976.blockButtons .blockTitle:not(.custom):hover .ffIcon {
5977 opacity: 1;
5978}
5979.task:not(.task_focused) .blockTitle:not(.tabScroller) {
5980 display: none;
5981}
5982.btnDarkSide {
5983 display: none;
5984}
5985.task:first-child .btnDarkSide {
5986 display: block;
5987}
5988.task-suite:not(.verticalSuite) .task {
5989 max-height: 100vh;
5990 overflow-y: scroll;
5991 margin: 0;
5992 min-height: 100vh;
5993}
5994.task-suite {
5995 padding: 0;
5996 margin: 0;
5997 width: 100000px;
5998 min-height: 100vh;
5999 display: block;
6000 background: #f2f2f2;
6001 background-attachment: fixed !important;
6002 position: relative;
6003 left: -000vw;
6004 transition: left 0.3s ease-out;
6005}
6006.task {
6007 margin: 0 0 40px;
6008 display: inline-block;
6009 width: 100vw;
6010 background: none;
6011 border: 0 none !important;
6012 box-shadow: none !important;
6013 text-align: center;
6014 opacity: 0.7;
6015 transition: all .3s ease-in 0s;
6016}
6017.task.task_focused {
6018 opacity: 1;
6019}
6020/*@media (min-width: 1600px) {
6021 .task {
6022 width: calc(50% - 5px);
6023 }
6024}*/
6025.task:last-of-type {
6026 margin-bottom: 12px;
6027}
6028.task-suite.darkSide {
6029 /*background:
6030 radial-gradient(black 15%, transparent 16%) 0 0,
6031 radial-gradient(black 15%, transparent 16%) 8px 8px,
6032 radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 0 1px,
6033 radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 8px 9px;*/
6034 background-color: #253548;
6035 background-size:16px 16px;
6036}
6037.darkSide .block {
6038 /*background: rgba(16, 32, 85, 0.53);*/
6039 background:#23272B;
6040 color: #DCD8C7;
6041}
6042.darkSide .blockTitle {
6043 background-color: #E0DAC0;
6044 background-size: 20px 20px;
6045 color: #262626;
6046}
6047.darkSide .btn a {
6048 color: #262626;
6049 background: #E0DAC0;
6050 border: 1px solid #E0DAC0;
6051 font-weight: 400;
6052}
6053.darkSide .btn:hover a {
6054 border: 1px solid #585B5E;
6055 box-shadow: 0 0 0 0;
6056 background: #585B5E !important;
6057 color: #FFFBEA;
6058}
6059.darkSide .btn.active a {
6060 background: #4A4E52;
6061 color:#FFFBEA;
6062 border: 1px solid #585B5E;
6063}
6064.darkSide td {
6065 color: #e6e6e6;
6066}
6067.darkSide input, .darkSide textarea {
6068 background: #4A4E52 !important;
6069 border: 1px solid #585B5E !important;
6070 color: #FFFBEA !important;
6071}
6072.darkSide input::placeholder, .darkSide textarea::placeholder {
6073 color: #e6e6e6 !important;
6074}
6075.darkSide input::-webkit-input-placeholder, .darkSide textarea::-webkit-input-placeholder {
6076 color: #e6e6e6;
6077}
6078.darkSide input[disabled], .darkSide textarea[disabled] {
6079 color: #899094 !important;
6080}
6081.darkSide input.validationRejected, .darkSide textarea.validationRejected {
6082 box-shadow: 0px 0px 6px 0px #f33;
6083}
6084.darkSide input.validationPassed, .darkSide textarea.validationPassed {
6085 box-shadow: 0px 0px 6px 0px #3fbd4c;
6086}
6087.darkSide .icon_upload {
6088 background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='14' height='13' viewBox='0 0 14 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 5v5H5V5H2l5-5 5 5H9zm-9 6h14v2H0v-2z' fill='%23999' fill-rule='evenodd'/%3E%3C/svg%3E");
6089}
6090.hiddenField label {
6091 display: none;
6092}
6093.entityBlock, .classBlock {
6094 user-select: none;
6095}
6096i.material-icons {
6097 opacity: 0.9;
6098}
6099input {
6100 display: block;
6101 margin: 5px 0 0px;
6102 width: 100%;
6103 opacity: 1;
6104 border: 1px solid #d9d9d9;
6105 border-radius: 3px;
6106 padding: 5px;
6107 font-size: 14px;
6108}
6109textarea {
6110 display: block;
6111 width: 100%;
6112 min-height: 50px;
6113 max-height: 600px;
6114 margin-top: 5px;
6115 border: 1px solid #d9d9d9 !important;
6116 border-radius: 3px;
6117 padding: 5px;
6118 font-size: 14px;
6119}
6120textarea:focus {
6121 border: 1px solid #b3b3b3;
6122}
6123table tr td:last-child {
6124 font-size: 0.9em;
6125}
6126.clickableLinkTitle, .labelTitle {
6127 font-size: 14px;
6128 font-weight: 400;
6129}
6130.blockContent .label, .blockContent .link {
6131 margin-bottom: 10px;
6132 font-size: 14px;
6133 font-weight: 400;
6134}
6135.blockContent {
6136 width: 100%;
6137}
6138.stepTitle.title {
6139 margin-bottom: 10px;
6140}
6141/*.stepTitle.title:first-letter {
6142 color: #000;
6143 \tfont-size: 120%;
6144 \tfont-weight: 600;
6145}*/
6146.stepQuestion, .stepTitle {
6147 word-break: break-word;
6148}
6149.link {
6150 margin-bottom: 5px;
6151 word-wrap: break-word;
6152}
6153.link a {
6154 color: #A24F00;
6155 font-weight: 600;
6156 word-break: break-all;
6157}
6158.link i {
6159 font-size: 18px;
6160 position: relative;
6161 top: 3px;
6162 margin-right: 3px;
6163}
6164.darkSide .link a, .darkSide a {
6165 color: #ec7e00;
6166}
6167input.validationRejected, textarea.validationRejected {
6168 box-shadow: 0px 0px 6px 0px #f33;
6169}
6170input.validationPassed, textarea.validationPassed {
6171 box-shadow: 0px 0px 6px 0px #3fbd4c;
6172}
6173.groupContainer {
6174 max-height: 10000px;
6175 opacity: 1;
6176 transition: all .3s ease-in-out 0s;
6177 vertical-align: top;
6178}
6179.groupContainer.excluded {
6180 overflow: hidden;
6181}
6182.inline {
6183 display: inline-block;
6184}
6185.groupContainer.inline .group {
6186 border: 0 none;
6187 display: inline-block;
6188 padding: 0;
6189}
6190.groupContainer.inline.excluded {
6191 max-width: 0;
6192}
6193.groupContainer.infinite .groupContainer.infinite {
6194 margin-left: 20px;
6195}
6196.group.infiniteChild.infiniteSkipped {
6197 opacity: 0.6;
6198}
6199.buttDiv.inline {
6200 display: inline-block;
6201}
6202.excluded {
6203 max-height: 0 !important;
6204 opacity: 0;
6205}
6206.block.excluded, .block.main .block.excluded {
6207 margin: 0 !important;
6208 padding: 0 !important;
6209}
6210.switchButton>i {
6211 z-index: 2;
6212 font-size: 23px;
6213 top: calc(50% - 10px);
6214 left: 6px;
6215 color: #404040;
6216 opacity: 0;
6217 transition: all .3s ease-in-out 0s;
6218}
6219.switchButton.active>i {
6220 opacity: 0.9;
6221}
6222.darkSide .switchButton>i {
6223 color: #FFFBEA;
6224}
6225.switchButton.active a, .linkButton a {
6226 padding-left: 30px;
6227}
6228/*.stepTitle a {
6229 color: darkred;
6230 font-weight: 500;
6231}
6232.darkSide .stepTitle a {
6233 color: greenyellow;
6234}*/
6235.inputData > .stepTitle {
6236 font-weight: 600;
6237}
6238.title {
6239 margin: 1px 0 0px;
6240}
6241.element {
6242 padding: 9px;
6243 border-radius: 3px;
6244 margin-top: 2px;
6245 position: relative;
6246}
6247.element.focused {
6248 /*background: #57738033;*/
6249 background: rgba(0, 0, 0, 0.05);
6250}
6251.darkSide .element.focused {
6252 background: rgba(255, 255, 255, 0.07);
6253}
6254.elementTitle {
6255 font-weight: 600;
6256 font-size: 15px;
6257}
6258.hotkeyHint {
6259 position: absolute;
6260 right: 4px;
6261 top: 7px;
6262 font-size: 9px;
6263 opacity: 0;
6264 color: #888;
6265 z-index: 2;
6266}
6267.focused .hotkeyHint {
6268 opacity: 1;
6269 transition: all .9s ease-out 0s;
6270}
6271/*.butt {
6272 display: inline-block;
6273 margin-right: 6px;
6274 margin-left: 10px;
6275 margin-bottom: 5px;
6276 transition: max-height .3s ease-in, max-width .3s ease-in, opacity .2s ease-in;
6277 max-height: 50px;
6278 opacity: 1;
6279 cursor: pointer;
6280 position: relative;
6281 user-select: none;
6282}
6283.butt a {
6284 font-size: 15px;
6285 color: #404040;
6286 display: inline-block;
6287 padding: 8px 12px;
6288 background: #fc0;
6289 margin: 4px 0 0;
6290 border-radius: 3px;
6291 border: 1px solid #fc0;
6292 cursor: pointer;
6293 font-weight: 400;
6294 white-space: nowrap;
6295 position: relative;
6296 min-width: 16px;
6297 transition: all .3s ease-in-out 0s;
6298 text-align: center;
6299 max-width: calc(100vw - 80px);
6300 overflow: hidden;
6301 height: 32px;
6302 line-height: 14px;
6303}
6304.butt a:hover {
6305 background:#f2c200;
6306 border: 1px solid #f2c200;
6307 box-shadow: 0 0 10px 1px #ffe478;
6308}
6309.darkSide .butt a {
6310 border: 1px solid #E0DAC0;
6311 background: #E0DAC0;
6312 color: #262626;
6313}
6314.darkSide .butt a:hover {
6315 background:#e0e3e5;
6316 border: 1px solid #e0e3e5;
6317 box-shadow: 0 0 8px 1px #7296a8;
6318}*/
6319.task.noErrors .task__error {
6320 display: none;
6321}
6322.field_file__label {
6323 height: 15px;
6324}
6325.stepTitle.title {
6326 font-weight: 400;
6327 font-size: 14px;
6328 margin-bottom: 1px;
6329}
6330.linkButtonIconContainer {
6331 background: none;
6332 width: 24px;
6333 height: 24px;
6334 overflow: hidden;
6335 border-radius: 12px;
6336 position: absolute;
6337 top: 8px;
6338 left: 4px;
6339 opacity: 0.8;
6340 user-select: none;
6341 z-index: 2;
6342}
6343.darkSide .linkButtonIconContainer {
6344 background: none;
6345}
6346.linkButton i {
6347 display: inline-block;
6348 width: 17px;
6349 height: 17px;
6350 margin: 2px 3px;
6351 background-repeat: no-repeat;
6352}
6353
6354/* Новые чекбоксы и радиобаттоны */
6355.checkboxGroup:not(.asButtons) .stepTitle {
6356 margin-bottom: 4px;
6357}
6358.checkboxGroup:not(.asButtons) .btn>i {
6359 top: 0px;
6360 left: -1px;
6361}
6362.checkboxGroup:not(.asButtons) .btn a {
6363 border: 0 none;
6364 background: none;
6365 margin: 0;
6366 padding: 1px 15px 1px 0px;
6367}
6368.checkboxGroup:not(.asButtons) .btn a:before {
6369 content: "";
6370 display: inline-block;
6371 background: #fff;
6372 width: 15px;
6373 height: 15px;
6374 border-radius: 3px;
6375 margin-right: 8px;
6376 position: relative;
6377 top: 3px;
6378 border: 1px solid #80808080;
6379}
6380.checkboxGroup:not(.asButtons) .btn.active a:before {
6381 background: #ffe478;
6382}
6383.checkboxGroup:not(.asButtons) .btn:hover a {
6384 box-shadow: none;
6385}
6386.darkSide .checkboxGroup:not(.asButtons) .btn.active a, .darkSide .checkboxGroup:not(.asButtons) .btn a {
6387 background: none;
6388 border: none;
6389 color: #FFFBEA;
6390}
6391.darkSide .checkboxGroup:not(.asButtons) .btn>i {
6392 color: #000;
6393 width: 19px;
6394 left: -1px;
6395 top: 1px;
6396}
6397
6398.radioGroup:not(.asButtons) .stepTitle {
6399 margin-bottom: 4px;
6400}
6401.radioGroup:not(.asButtons) .btn>i {
6402 top: 10px;
6403 left: 6px;
6404 width: 7px;
6405 background: #000;
6406 height: 7px;
6407 border-radius: 5px;
6408}
6409.radioGroup:not(.asButtons) .btn>i svg {
6410 display: none;
6411}
6412.radioGroup:not(.asButtons) .btn a {
6413 border: 0 none;
6414 background: none;
6415 margin: 0;
6416 padding: 1px 15px 1px 0px;
6417}
6418.radioGroup:not(.asButtons) .btn a:before {
6419 content: "";
6420 display: inline-block;
6421 background: #fff;
6422 width: 17px;
6423 height: 17px;
6424 border-radius: 17px;
6425 margin-right: 8px;
6426 position: relative;
6427 top: 3px;
6428 border: 1px solid #80808080;
6429}
6430.radioGroup:not(.asButtons) .btn.active a:before {
6431 background: #ffe478;
6432}
6433.radioGroup:not(.asButtons) .btn:hover a {
6434 box-shadow: none;
6435}
6436.darkSide .radioGroup:not(.asButtons) .btn.active a, .darkSide .radioGroup:not(.asButtons) .btn a {
6437 background: none;
6438 border: none;
6439 color: #FFFBEA;
6440}
6441
6442/* Переопределения для вертикального сьюта */
6443.task-suite.verticalSuite {
6444 width: 100%;
6445 overflow-x: hidden;
6446}
6447
6448
6449/* Перелистывание вкладок */
6450.tabScroller {
6451 right: 18px;
6452 height: 23px !important;
6453 padding-top: 5px !important;
6454 z-index: 200;
6455 user-select: none;
6456}
6457.tabScroller i {
6458 line-height: 0;
6459 font-size: 32px;
6460 font-weight: 500;
6461 display: none;
6462 margin: -10px 0;
6463}
6464.leftScroll i:nth-of-type(2) {
6465 transform: rotate(180deg);
6466}
6467.rightScroll i:first-child {
6468 transform: rotate(180deg) !important;
6469}
6470.tabScroller i:nth-of-type(odd) {
6471 display: inline-block;
6472}
6473.task:first-of-type .tabScroller .leftScroll i:first-of-type, .task:last-of-type .tabScroller .rightScroll i:first-of-type{
6474 display: none;
6475}
6476.task:first-of-type .tabScroller .leftScroll i:last-of-type, .task:last-of-type .tabScroller .rightScroll i:last-of-type {
6477 display: inline-block;
6478}
6479.curTab, .tabsCount {
6480 margin: 0px 0px;
6481}
6482.leftScroll i:last-of-type, .rightScroll i:last-of-type {
6483 color: #808080;
6484}
6485.leftScroll, .rightScroll {
6486 cursor: pointer;
6487}
6488.tabNumbers {
6489 font-size: 16px;
6490 font-weight: 600;
6491 margin: 0 10px;
6492 top: 1px;
6493 position: relative;
6494 font-family: sans-serif;
6495 opacity: 0.8;
6496}
6497.verticalSuite .leftScroll svg, .verticalSuite .rightScroll svg {
6498 transform: rotate(90deg);
6499}
6500.verticalSuite .tabScroller {
6501 padding-left: 0;
6502 padding-right: 0;
6503}
6504.tabScroller .ffIcon {
6505 opacity: 0.6;
6506 transition: .3s ease-out;
6507}
6508.tabScroller:hover .ffIcon {
6509 opacity: 1;
6510}
6511
6512/* Сворачивалка блоков */
6513.block.minimizable .blockTitle {
6514 cursor: pointer;
6515}
6516.miniMaxi {
6517 user-select: none;
6518}
6519.miniMaxi i {
6520 font-size: 24px;
6521 transform: rotate(180deg);
6522 transition: all .2s ease-in 0s;
6523 padding: 0px;
6524 opacity: 0.8;
6525 margin: -7px -3px -7px 4px;
6526}
6527.minimized .miniMaxi i {
6528 transform: rotate(270deg);
6529}
6530.block:not(.minimizable)>div>div>.miniMaxi {
6531 display: none;
6532}
6533.blockContent {
6534 /* max-height: 1500px; */
6535 transition: max-height .5s ease-out 0s, overflow 0s ease 1s;
6536 overflow: visible;
6537}
6538.blockContent.minimized {
6539 max-height: 0px;
6540 overflow: hidden;
6541}
6542.blockContent>.helpContainer {
6543 position: absolute;
6544 left: auto;
6545 right: 3px;
6546 top: 2px;
6547}
6548.block:not(:hover)>.blockContent>.helpContainer:not(.triggered) {
6549 opacity: 0;
6550}
6551.minimized .searchField {
6552 opacity: 0;
6553}
6554.popUpTitle {
6555 padding: 4px 8px;
6556 position: absolute;
6557 left: 0px;
6558 top: 15px;
6559 color: black;
6560 white-space: pre-line;
6561 width: 400px;
6562 font-size: 14px;
6563 border: 1px solid #A6A6A6;
6564 z-index: 3000;
6565 border-radius: 5px;
6566 background: #fffe;
6567 opacity: 0;
6568 transition: opacity .3s ease-in-out 0s, visibility 0s ease 1.2s;
6569 visibility: hidden;
6570 box-shadow: 0 0 10px #000A;
6571}
6572div:not(.helpContainer)>.popUpTitle:hover {
6573 display: none;
6574}
6575/*.searchField .popUpTitle {
6576 line-height: initial;
6577}*/
6578.btn .popUpTitle {
6579 top: 40px;
6580}
6581.elementBody .popUpTitle {
6582 top: 40px;
6583}
6584.upperPopups .popUpTitle {
6585 top: initial;
6586 bottom: 35px;
6587}
6588.darkSide .popUpTitle {
6589 background: #222e;
6590 border-color: #666;
6591 color: snow;
6592}
6593.helpContainer {
6594 vertical-align: top;
6595 display: inline-block;
6596 position: relative;
6597 left: 2px;
6598 font-size: 15px;
6599 cursor: default;
6600 opacity: 1;
6601 top: 5px;
6602 transition: all .3s ease-out 0s;
6603}
6604.helpContainer .ffIcon {
6605 cursor: pointer;
6606 padding: 0;
6607 color: #A6A6A6;
6608 margin: 0;
6609 transition: color .3s ease-in;
6610 top: -1px;
6611 position: relative;
6612}
6613.blockContent>.helpContainer .ffIcon {
6614 width: 1.2em;
6615 height: 1.2em;
6616}
6617.helpContainer.triggered .popUpTitle, .helpContainer.openedByKey .popUpTitle, .openedByKey .popUpTitle {
6618 opacity: 1;
6619 visibility: visible;
6620 transition: opacity .3s ease-in-out 0s, visibility 0s ease 0s;
6621}
6622.helpContainer .popUpTitle {
6623 transition: opacity .3s ease-in-out 0s, visibility 0s ease .3s;
6624 display: flex;
6625 padding: 1px;
6626}
6627/*.element:not(.focused):not(:hover) .helpContainer {
6628 transition: none;
6629}*/
6630.helpContainer .ffIcon:hover, .helpContainer.openedByKey .ffIcon {
6631 color: #737373;
6632}
6633.darkSide .helpContainer .ffIcon {
6634 color: #808080;
6635}
6636.darkSide .helpContainer .ffIcon:hover, .darkSide .helpContainer.openedByKey .ffIcon {
6637 color: #BFBFBF;
6638}
6639.element:not(.focused):not(:hover) .helpContainer {
6640 opacity: 0;
6641}
6642.elementTitle>.stepQuestion>.helpContainer, .element.fileField>.stepTitle>.helpContainer {
6643 display: inline-block;
6644 top: 1px;
6645}
6646.element>.stepTitle>.helpContainer {
6647 top: 2px;
6648}
6649.element:not(:hover) .elementTitle>.stepQuestion>.helpContainer:not(.triggered) {
6650 opacity: 0;
6651}
6652.buttPath {
6653 font-weight: 600;
6654}
6655.elementBody {
6656 position: relative;
6657}
6658.cursorHelp {
6659 cursor: help;
6660}
6661.popUpTitleInner {
6662 overflow-y: auto;
6663 padding: 3px 7px;
6664 word-break: break-word;
6665}
6666/*.suggest:hover .popUpTitle, */.btn:hover .popUpTitle, .elementBody:hover .popUpTitle, .selectHead:hover .popUpTitle {
6667 opacity: 1;
6668 transition: opacity .3s ease-in-out .9s, visibility 0s ease .9s;
6669 visibility: visible;
6670 }
6671/*.suggest.final {
6672 order: 0;
6673 background: #cdf9c4;
6674}
6675.suggest.final:hover {
6676 background: #88ff71;
6677}
6678.suggest.notFinal {
6679 order: 1;
6680 background: #ffefb2;
6681}
6682.suggest.notFinal:hover {
6683 background: #ffdb4d;
6684}
6685.darkSide .suggest.final {
6686 background: #386b00;
6687}
6688.darkSide .suggest.notFinal {
6689 background: #715a00;
6690}
6691.darkSide .suggest.notFinal:hover {
6692 background: #a98700;
6693}
6694.darkSide .suggest.final:hover {
6695 background: #5baf00;
6696}*/
6697.img__container__imitation {
6698 background-position: 0%;
6699}
6700
6701/* Вывод ошибок */
6702.errorMsgBlock {
6703 position: absolute;
6704 color: white;
6705 bottom: 100%;
6706 background: #fa7067;
6707 padding: 2px 2px;
6708 font-size: 14px;
6709 border-width: 7px;
6710 display: none;
6711 white-space: pre-line;
6712 z-index: 1000;
6713}
6714.withSomeError .errorMsgBlock {
6715 display: flex;
6716}
6717.errorMsgBlock:after {
6718 border-width: 9px;
6719 content: " ";
6720 bottom: -18px;
6721 z-index: 100;
6722 border-style: solid;
6723 pointer-events: none;
6724 left: 10px;
6725 height: 0;
6726 width: 0;
6727 background: 0 none;
6728 position: absolute;
6729 border-color: #fa7067 #0000 #0000;
6730}
6731.errorMsgBlockInner {
6732 overflow-y: auto;
6733 max-height: 100%;
6734 padding: 2px 5px 2px 5px;
6735}
6736.darkSide .errorMsgBlock {
6737 background: #fb5c56;
6738}
6739.darkSide .errorMsgBlock:before {
6740 border-color: #fb5c56 #0000 #0000;
6741}
6742.element.withSomeError {
6743 background-color: #f9baaf33;
6744}
6745.element.focused.withSomeError {
6746 background-color: #b7585833;
6747}
6748.darkSide .element.withSomeError {
6749 background-color: #ff000014;
6750}
6751.darkSide .element.focused.withSomeError {
6752 background-color: #ff19192e;
6753}
6754
6755/* Список тасков в сьюте */
6756.taskName {
6757 display: none;
6758}
6759.tabList {
6760 width: max-content;
6761 position: absolute;
6762 right: 0px;
6763 transition: max-height .2s cubic-bezier(0, 0.3, 0.24, 0.59) 0s;
6764 max-width: 50vw;
6765 cursor: default;
6766 overflow: hidden;
6767}
6768.tabScroller:hover .tabList {
6769 transition: max-height .2s cubic-bezier(0, 0.3, 0.24, 0.59) 0.7s;
6770}
6771.tabList>div {
6772 z-index: 1000;
6773 border: 1px solid #81c45600;
6774 border-radius: 5px;
6775 padding: 5px 0;
6776 position: relative;
6777 margin-top: 10px;
6778 top: 0;
6779 background: white;
6780 box-shadow: 5px 5px 18px #0002;
6781}
6782.tabListItemNumber, .tabListItemName {
6783 display: inline-block;
6784 margin: 1px 7px 1px 14px;
6785}
6786.tabListItemNumber {
6787 font-size: 11px;
6788 position: absolute;
6789 left: -10px;
6790 top: 4px;
6791}
6792.tabListItem {
6793 opacity: 0.7;
6794 padding: 3px 0px 3px 4px;
6795 font-weight: 400;
6796 position: relative;
6797 margin: 2px 5px 2px 4px;
6798 cursor: pointer;
6799}
6800.tabListItem.selected {
6801 color: #404040;
6802 opacity: 0.8;
6803 font-weight: 600;
6804 cursor: default;
6805 background: #ffe478;
6806 border-radius: 3px;
6807}
6808.tabListItem:not(.selected):hover {
6809 opacity: 1;
6810}
6811.darkSide .tabList>div {
6812 border-color: #7296a8;
6813 background-color: #E0DAC0;
6814 color: #3a454b;
6815}
6816.darkSide .tabListItem.selected {
6817 color: #253548;
6818 opacity: 0.93;
6819}
6820.task:not(.task_focused) .tabList>div {
6821 border-color: #cccccc;
6822}
6823.task:not(.task_focused) .tabListItem.selected {
6824 background-color: #d9d9d9;
6825}
6826/* Директ */
6827.no_notForDirect .bannerContainer {
6828 border: 1px solid #bbb;
6829 padding: 10px;
6830 width: fit-content;
6831 border-radius: 2px;
6832 display: inline-block;
6833 text-align: left;
6834}
6835.no_notForDirect .bannerTitle {
6836 font-size: 16px;
6837 line-height: 20px;
6838 position: relative;
6839 font-weight: bold;
6840 margin-bottom: 5px;
6841 display: flex;
6842}
6843.no_notForDirect.darkSide .bannerTitle a {
6844 color: aqua;
6845}
6846.no_notForDirect .bannerDomain {
6847 font-size: 12px;
6848 line-height: 16px;
6849 color: rgb(0, 102, 0);
6850 font-weight: bold;
6851 margin-top: 8px;
6852}
6853.no_notForDirect.darkSide .bannerDomain {
6854 color: #36e400;
6855}
6856.no_notForDirect .fieldName_verdict .buttDiv.fieldName_Yes {
6857 width: calc(25% - 7px);
6858}
6859.no_notForDirect .fieldName_checkboxes {
6860 margin-left: 25%;
6861}
6862.no_notForDirect .bannerInfoLine>div {
6863 display: inline-block;
6864 margin-right: 5px;
6865}
6866.no_notForDirect .bannerInfoLine {
6867 margin-bottom: 3px;
6868}
6869.no_notForDirect .bilValue {
6870 font-weight: 600;
6871 margin: 0 !important;
6872}
6873.no_notForDirect .topFlexBox {
6874 display: flex;
6875 flex-wrap: wrap;
6876}
6877.no_notForDirect .bannerElement {
6878 flex-grow: 10;
6879 text-align: center;
6880 padding-top: 0;
6881 margin-right: 20px;
6882}
6883.no_notForDirect .bannerInfoElement {
6884 background: #81c456;
6885 padding: 7px 15px;
6886 border-radius: 5px;
6887 border: 1px solid #81c456;
6888 flex-shrink: 0;
6889 margin-left: 9px;
6890 margin-bottom: 9px;
6891}
6892.no_notForDirect .bannerTitle a {
6893 color: blue;
6894 transition: all .1s ease-in;
6895 opacity: 1;
6896}
6897.no_notForDirect .bannerTitle .ffIcon {
6898 display: none;
6899}
6900.no_notForDirect .bannerTitle.visited .ffIcon {
6901 display: block;
6902}
6903.no_notForDirect .bannerTitle a:hover {
6904 opacity: 0.75;
6905}
6906.no_notForDirect .bannerTitle.visited a {
6907 color: #7000ff;
6908}
6909.no_notForDirect.darkSide .bannerTitle.visited a {
6910 color: #b843ff;
6911}
6912.no_notForDirect .bannerTitle .ffIcon {
6913 font-size: 25px;
6914 position: relative;
6915 top: -3px;
6916 left: -2px;
6917 color: #bbb;
6918 margin: 0 -1px;
6919}
6920.no_notForDirect .popUpTitle {
6921 width: 555px;
6922}
6923@media (max-width: 768px) {
6924 .no_notForDirect .block {
6925 max-width: initial;
6926 }
6927 .helpContainer {
6928 display: none;
6929 }
6930}
6931.misprinted {
6932 color: red;
6933}
6934
6935/* SVGшки */
6936.yandexButtIcon i {
6937 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path fill="tomato" d="M153.1 315.8L65.7 512H2l96-209.8c-45.1-22.9-75.2-64.4-75.2-141.1C22.7 53.7 90.8 0 171.7 0H254v512h-55.1V315.8h-45.8zm45.8-269.3h-29.4c-44.4 0-87.4 29.4-87.4 114.6 0 82.3 39.4 108.8 87.4 108.8h29.4V46.5z"></path></svg>');
6938 margin-left: 6px;
6939}
6940.googleButtIcon i {
6941 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 488 512"><path fill="royalblue" d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"></path></svg>');
6942 left: -1px;
6943}
6944.undoButtIcon i {
6945 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M129 383a12 12 0 0 1 16.37-.56A166.77 166.77 0 0 0 256 424c93.82 0 167.24-76 168-166.55C424.79 162 346.91 87.21 254.51 88a166.73 166.73 0 0 0-113.2 45.25L84.69 76.69A247.12 247.12 0 0 1 255.54 8C392.35 7.76 504 119.19 504 256c0 137-111 248-248 248a247.11 247.11 0 0 1-166.18-63.91l-.49-.46a12 12 0 0 1 0-17z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M49 41l134.06 134c15.09 15.15 4.38 41-17 41H32a24 24 0 0 1-24-24V57.94C8 36.56 33.85 25.85 49 41z"></path></g></svg>');
6946 margin-left: 1px;
6947}
6948.redoButtIcon i {
6949 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M422.66 422.66a12 12 0 0 1 0 17l-.49.46A247.11 247.11 0 0 1 256 504C119 504 8 393 8 256 8 119.19 119.65 7.76 256.46 8a247.12 247.12 0 0 1 170.85 68.69l-56.62 56.56A166.73 166.73 0 0 0 257.49 88C165.09 87.21 87.21 162 88 257.45 88.76 348 162.18 424 256 424a166.77 166.77 0 0 0 110.63-41.56A12 12 0 0 1 383 383z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M504 57.94V192a24 24 0 0 1-24 24H345.94c-21.38 0-32.09-25.85-17-41L463 41c15.15-15.15 41-4.44 41 16.94z"></path></g></svg>');
6950 margin-left: 0px;
6951}
6952.syncButtIcon i {
6953 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M8 454.06V320a24 24 0 0 1 24-24h134.06c21.38 0 32.09 25.85 17 41l-41.75 41.75A166.82 166.82 0 0 0 256.16 424c77.41-.07 144.31-53.14 162.78-126.85a12 12 0 0 1 11.65-9.15h57.31a12 12 0 0 1 11.81 14.18C478.07 417.08 377.19 504 256 504a247.14 247.14 0 0 1-171.31-68.69L49 471c-15.15 15.15-41 4.44-41-16.94z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M12.3 209.82C33.93 94.92 134.81 8 256 8a247.14 247.14 0 0 1 171.31 68.69L463 41c15.12-15.12 41-4.41 41 17v134a24 24 0 0 1-24 24H345.94c-21.38 0-32.09-25.85-17-41l41.75-41.75A166.8 166.8 0 0 0 255.85 88c-77.46.07-144.33 53.18-162.79 126.85A12 12 0 0 1 81.41 224H24.1a12 12 0 0 1-11.8-14.18z"></path></g></svg>');
6954 margin-left: 1px;
6955}
6956.adjustButtIcon i {
6957 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><g><path class="fa-secondary" fill="currentColor" d="M568 256c0 137-111 248-248 248V8c137 0 248 111 248 248z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M320 8v496C183 504 72 393 72 256S183 8 320 8z"></path></g></svg>');
6958 margin-left: 1px;
6959 margin-top: 3px;
6960 width: 21px;
6961 height: 21px;
6962}
6963.adjustSolidButtIcon i {
6964 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><g><path class="fa-secondary" fill="currentColor" d="M545.3 226L439.6 329l25 145.5c4.5 26.1-23 46-46.4 33.7l-130.7-68.6V0a31.62 31.62 0 0 1 28.7 17.8l65.3 132.4 146.1 21.2c26.2 3.8 36.7 36.1 17.7 54.6z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M110.4 474.5l25-145.5L29.7 226c-19-18.5-8.5-50.8 17.7-54.6l146.1-21.2 65.3-132.4A31.62 31.62 0 0 1 287.5 0v439.6l-130.7 68.6c-23.4 12.3-50.9-7.6-46.4-33.7z"></path></g></svg>');
6965 margin-left: 1px;
6966 width: 18px;
6967 height: 18px;
6968}
6969.swatchBookIcon i {
6970 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M435.56 167.1l-90.33-90.51a31.89 31.89 0 0 0-45.1-.07l-.07.07-75.5 75.65V416c0 3-.67 5.73-.87 8.64l211.87-212.28a32.05 32.05 0 0 0 0-45.26zM63.88 192v64h63.93v-64zm416.18 128H373.29L187.15 506.51c-2.06 2.07-4.49 3.58-6.67 5.49h299.58A32 32 0 0 0 512 480V352a32 32 0 0 0-31.94-32zM63.88 64v64h63.93V64z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M159.68 0H31.94A32 32 0 0 0 0 32v384a95.81 95.81 0 1 0 191.62 0V32a32 32 0 0 0-31.94-32zM95.81 440a24 24 0 1 1 24-24 24 24 0 0 1-24 24zM63.88 256v-64h63.88v64zm0-128V64h63.88v64z"></path></g></svg>');
6971 margin-left: 1px;
6972 width: 18px;
6973 height: 18px;
6974}
6975.sunButtIcon i {
6976 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M502.42 240.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.41-94.8a17.31 17.31 0 0 0-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4a17.31 17.31 0 0 0 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.41-33.5 47.3 94.7a17.31 17.31 0 0 0 31 0l47.31-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3a17.33 17.33 0 0 0 .2-31.1zm-155.9 106c-49.91 49.9-131.11 49.9-181 0a128.13 128.13 0 0 1 0-181c49.9-49.9 131.1-49.9 181 0a128.13 128.13 0 0 1 0 181z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M352 256a96 96 0 1 1-96-96 96.15 96.15 0 0 1 96 96z"></path></g></svg>');
6977 margin-left: 1px;
6978 width: 18px;
6979 height: 18px;
6980}
6981.paletteButtIcon i {
6982 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M204.29 5c-99.4 19.4-179.5 99.29-199.1 198.4-37 187 131.7 326.39 258.8 306.69 41.2-6.4 61.4-54.59 42.5-91.69-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.31C511.49 97.13 368.09-26.87 204.29 5zM96 320a32 32 0 1 1 32-32 32 32 0 0 1-32 32zm32-128a32 32 0 1 1 32-32 32 32 0 0 1-32 32zm128-64a32 32 0 1 1 32-32 32 32 0 0 1-32 32zm128 64a32 32 0 1 1 32-32 32 32 0 0 1-32 32z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M96 256a32 32 0 1 0 32 32 32 32 0 0 0-32-32zm32-128a32 32 0 1 0 32 32 32 32 0 0 0-32-32zm128-64a32 32 0 1 0 32 32 32 32 0 0 0-32-32zm128 64a32 32 0 1 0 32 32 32 32 0 0 0-32-32z"></path></g></svg>');
6983 margin-left: 1px;
6984 width: 18px;
6985 height: 18px;
6986}
6987.expandButtIcon i {
6988 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><g><path class="fa-secondary" fill="currentColor" d="M148 32H24A23.94 23.94 0 0 0 0 56v124a12 12 0 0 0 12 12h40a12 12 0 0 0 12-12V96h84a12 12 0 0 0 12-12V44a12 12 0 0 0-12-12zm288 288h-40a12 12 0 0 0-12 12v84h-84a12 12 0 0 0-12 12v40a12 12 0 0 0 12 12h124a23.94 23.94 0 0 0 24-24V332a12 12 0 0 0-12-12z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M148 416H64v-84a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12v124a23.94 23.94 0 0 0 24 24h124a12 12 0 0 0 12-12v-40a12 12 0 0 0-12-12zM424 32H300a12 12 0 0 0-12 12v40a12 12 0 0 0 12 12h84v84a12 12 0 0 0 12 12h40a12 12 0 0 0 12-12V56a23.94 23.94 0 0 0-24-24z"></path></g></svg>');
6989}
6990.washerButtIcon i {
6991 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 512"><g><path class="fa-secondary" fill="currentColor" d="M298 300a51.75 51.75 0 0 0 36.11-14.69A110.76 110.76 0 0 1 336 304a112 112 0 0 1-224 0 110.76 110.76 0 0 1 1.89-18.69 51.79 51.79 0 0 0 73.24-1 51.23 51.23 0 0 0 73.74 0A51.81 51.81 0 0 0 298 300z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M384 0H64A64 64 0 0 0 0 64v416a32 32 0 0 0 32 32h384a32 32 0 0 0 32-32V64a64 64 0 0 0-64-64zM184 64a24 24 0 1 1-24 24 24 24 0 0 1 24-24zM64 88a24 24 0 1 1 24 24 24 24 0 0 1-24-24zm160 360a144 144 0 1 1 144-144 144 144 0 0 1-144 144z"></path></g></svg>');
6992 margin-left: 1px;
6993}
6994.eraserButtIcon i {
6995 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path class="fa-secondary" fill="currentColor" d="M512 428v40a12 12 0 0 1-12 12H144a48 48 0 0 1-33.94-14.06l-96-96a48 48 0 0 1 0-67.88l136-136 227.88 227.88L355.88 416H500a12 12 0 0 1 12 12z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M377.94 393.94l120-120a48 48 0 0 0 0-67.88l-160-160a48 48 0 0 0-67.88 0l-120 120 45.25 45.25z"></path></g></svg>');
6996 margin-left: 1px;
6997}
6998.defaultButtIcon i {
6999 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 512"><path fill="grey" d="M448 279.196V464c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h248a24 24 0 0 1 16.97 7.029l16 16C344.09 102.149 333.382 128 312 128H64v320h320V295.196c0-6.365 2.529-12.47 7.029-16.971l16-16C422.148 247.106 448 257.814 448 279.196zM576 37.333C576 16.715 559.285 0 538.667 0H380c-15.464 0-28 12.536-28 28v17.885c0 15.766 13.011 28.424 28.772 27.989l67.203-1.906L199.09 319.09c-9.429 9.363-9.457 24.605-.061 34.001l23.879 23.879c9.396 9.396 24.639 9.369 34.001-.06l247.122-248.885-1.906 67.203c-.434 15.76 12.224 28.772 27.99 28.772H548c15.464 0 28-12.536 28-28V37.333z"></path></svg>');
7000 margin-top: 4px;
7001}
7002.uploadButtIcon i {
7003 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g><path fill="currentColor" d="M488 351.92H352v8a56 56 0 0 1-56 56h-80a56 56 0 0 1-56-56v-8H24a23.94 23.94 0 0 0-24 24v112a23.94 23.94 0 0 0 24 24h464a23.94 23.94 0 0 0 24-24v-112a23.94 23.94 0 0 0-24-24zm-120 132a20 20 0 1 1 20-20 20.06 20.06 0 0 1-20 20zm64 0a20 20 0 1 1 20-20 20.06 20.06 0 0 1-20 20z" opacity="0.4"></path><path fill="currentColor" d="M192 359.93v-168h-87.7c-17.8 0-26.7-21.5-14.1-34.11L242.3 5.62a19.37 19.37 0 0 1 27.3 0l152.2 152.2c12.6 12.61 3.7 34.11-14.1 34.11H320v168a23.94 23.94 0 0 1-24 24h-80a23.94 23.94 0 0 1-24-24z"></path></g></svg>');
7004 margin-left: 1px;
7005}
7006.thrashButtIcon i {
7007 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><g><path fill="currentColor" d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V96H32zm272-288a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0z" opacity="0.4"></path><path fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM128 160a16 16 0 0 0-16 16v224a16 16 0 0 0 32 0V176a16 16 0 0 0-16-16zm96 0a16 16 0 0 0-16 16v224a16 16 0 0 0 32 0V176a16 16 0 0 0-16-16zm96 0a16 16 0 0 0-16 16v224a16 16 0 0 0 32 0V176a16 16 0 0 0-16-16z"></path></g></svg>');
7008 margin-left: 1px;
7009}
7010.editButtIcon i {
7011 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><g class="fa-group"><path class="fa-secondary" fill="currentColor" d="M564.6 60.2l-48.8-48.8a39.11 39.11 0 0 0-55.2 0l-35.4 35.4a9.78 9.78 0 0 0 0 13.8l90.2 90.2a9.78 9.78 0 0 0 13.8 0l35.4-35.4a39.11 39.11 0 0 0 0-55.2zM427.5 297.6l-40 40a12.3 12.3 0 0 0-3.5 8.5v101.8H64v-320h229.8a12.3 12.3 0 0 0 8.5-3.5l40-40a12 12 0 0 0-8.5-20.5H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V306.1a12 12 0 0 0-20.5-8.5z" opacity="0.4"></path><path class="fa-primary" fill="currentColor" d="M492.8 173.3a9.78 9.78 0 0 1 0 13.8L274.4 405.5l-92.8 10.3a19.45 19.45 0 0 1-21.5-21.5l10.3-92.8L388.8 83.1a9.78 9.78 0 0 1 13.8 0z"></path></g></svg>');
7012 margin-left: 1px;
7013}
7014.kinopoiskButtIcon i { /* by @stillnoise */
7015 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="orangered" d="M8,16 C3.581722,16 0,12.418278 0,8 C0,3.581722 3.581722,0 8,0 C12.418278,0 16,3.581722 16,8 C16,12.418278 12.418278,16 8,16 Z M8,9 C8.55228475,9 9,8.55228475 9,8 C9,7.44771525 8.55228475,7 8,7 C7.44771525,7 7,7.44771525 7,8 C7,8.55228475 7.44771525,9 8,9 Z M5,7 C6.1045695,7 7,6.1045695 7,5 C7,3.8954305 6.1045695,3 5,3 C3.8954305,3 3,3.8954305 3,5 C3,6.1045695 3.8954305,7 5,7 Z M11,7 C12.1045695,7 13,6.1045695 13,5 C13,3.8954305 12.1045695,3 11,3 C9.8954305,3 9,3.8954305 9,5 C9,6.1045695 9.8954305,7 11,7 Z M11,13 C12.1045695,13 13,12.1045695 13,11 C13,9.8954305 12.1045695,9 11,9 C9.8954305,9 9,9.8954305 9,11 C9,12.1045695 9.8954305,13 11,13 Z M5,13 C6.1045695,13 7,12.1045695 7,11 C7,9.8954305 6.1045695,9 5,9 C3.8954305,9 3,9.8954305 3,11 C3,12.1045695 3.8954305,13 5,13 Z"></path></svg>');
7016}
7017.linkButtonIconContainer i {
7018 top: 0px;
7019 left: 2px;
7020}
7021
7022/* SVG-шные иконки */
7023.ffIcon {
7024 display: inline-block;
7025 width: 1em;
7026 height: 1em;
7027 padding: 3px;
7028 color: #111;
7029 box-sizing: border-box;
7030}
7031.ffIcon svg {
7032 display: block;
7033 width: 100%;
7034 height: 100%;
7035}
7036.darkSide .link .ffIcon {
7037 color: #e0dac0;
7038}
7039
7040/* Настройки шаблона */
7041.no_taskList .tabList {
7042 display: none;
7043}
7044.no_taskCounter .tabNumbers {
7045 width: 0 !important;
7046 overflow: hidden;
7047 display: inline-block;
7048 margin: 0 5px;
7049}
7050.no_taskSwitcher .tabScroller {
7051 display: none;
7052}
7053.no_taskMinimizer .miniMaxi {
7054 display: none;
7055}
7056.no_taskMinimizer .block.minimizable .blockTitle {
7057 cursor: default;
7058}
7059
7060/* Просмотрщик картинок (начиная с 1.20) */
7061.imageField {
7062 user-select: none;
7063}
7064.imageField .imageBlock {
7065 overflow: hidden;
7066 flex-grow: 10;
7067 position: relative;
7068}
7069.imageField .imageBlock img {
7070 position: relative;
7071}
7072.imageField .imageBlock img {
7073 position: relative;
7074 cursor: grab;
7075 transition: all .1s ease-out;
7076 transform: none;
7077}
7078.imageConsole {
7079 position: absolute;
7080 bottom: 0;
7081 right: 0;
7082 color: white;
7083 text-shadow: 0 0 10px black, 0 0 5px black;
7084 opacity: 0;
7085 transition: all .3s ease-out;
7086 margin: 0 2px 1px 0;
7087}
7088.imageField canvas {
7089 width: 100%;
7090 height: 400px;
7091 image-rendering: optimizeQuality;
7092 image-rendering: -moz-crisp-edges;
7093 background-image: linear-gradient(45deg, #8883 25%, transparent 25%),linear-gradient(-45deg, #8883 25%, transparent 25%),linear-gradient(45deg, transparent 75%, #8883 75%),linear-gradient(-45deg, transparent 75%, #8883 75%);
7094 background-size: 20px 20px;
7095 background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
7096 cursor: grab;
7097}
7098.dragging.imageField canvas {
7099 cursor: grabbing;
7100}
7101.imageField.expanded {
7102 position: fixed;
7103 z-index: 3000;
7104 background: white;
7105 width: calc(100vw - 50px) !important;
7106 height: calc(100vh - 50px);
7107 left: 10px;
7108 top: 10px;
7109 padding: 15px;
7110 box-shadow: 0 0 35px 0px #000;
7111 display: flex;
7112 flex-direction: column;
7113 margin: 0 !important;
7114}
7115.imageField.expanded canvas {
7116 height: 100% !important;
7117 width: 100% !important;
7118}
7119.imageField .stepTitle {
7120 text-align: center;
7121 margin-bottom: 8px;
7122}
7123.imageField.expanded .stepTitle {
7124 display: none;
7125}
7126.imageFooter {
7127 display: flex;
7128 justify-content: space-between;
7129}
7130.imageFooter .btn a {
7131 width: 0px;
7132 overflow: hidden;
7133 color: #0000;
7134 padding-right: 0;
7135 padding-left: 12px;
7136 z-index: 10;
7137 background: #0000;
7138 margin-top: 0;
7139 white-space: nowrap;
7140 height: 17px;
7141}
7142.imageFooter .linkButtonIconContainer {
7143 text-align: center;
7144 top: 4px;
7145 left: 4px;
7146 opacity: 0.9;
7147}
7148.imageSource {
7149 display: none;
7150}
7151.imageOriginalLink {
7152 display: none;
7153}
7154.darkSide .imageFooter .btn:hover a {
7155 background: #0000 !important;
7156 color: #0000;
7157}
7158.imageFooter>div>.element {
7159 padding: 0;
7160}
7161.imageFooter>div>.element .btn {
7162 margin-bottom: 0;
7163 margin-top: 3px;
7164}
7165.imageFooter>div>.element .buttDiv:last-child {
7166 margin-right: 0;
7167}
7168.imageFooter .hotkeyHint {
7169 right: 2px;
7170 font-size: 8px;
7171 top: 1px;
7172}
7173.darkSide .imageField.expanded.focused {
7174 background: rgb(56, 68, 74);
7175 border: 1px solid #7296a8;
7176}
7177
7178.darkSide .imageField .btn a {
7179 border: 0 none;
7180}
7181.imageFooter>div>.element .btn {
7182 border-radius: 3px;
7183 margin-top: 6px;
7184 width: 30px;
7185 height: 29px;
7186 transition: all .2s ease-out;
7187 background: #FFFBEA;
7188}
7189.darkSide .imageFooter>div>.element .btn {
7190 background: #E0DAC0;
7191}
7192.darkSide .imageFooter>div>.element .btn.selected {
7193 background: #4A4E52;
7194}
7195.imageFooter>div>.element .btn.selected {
7196 background: #ffe478;
7197}
7198.imageRightButts .popUpTitle {
7199 left: initial;
7200 right: 0;
7201}
7202.imageField.noCanvas .imageConsole {
7203 display: none;
7204}
7205.imageField.noCanvas canvas {
7206 position: absolute;
7207 left: 0;
7208}
7209.imageField.noCanvas .justAnImage {
7210 object-fit: contain;
7211 width: 100%;
7212 height: 100%;
7213}
7214.imageField.noCanvas .imageBlock {
7215 max-height: 400px;
7216 text-align: center;
7217}
7218.imageField.noCanvas.expanded .imageBlock {
7219 max-height: 100%;
7220}
7221/* Select - элемент с версии 1.22 */
7222.select.element {
7223 position: relative;
7224 min-width: 200px;
7225 width: 300px;
7226 display: block;
7227}
7228.select.element .stepTitle {
7229 margin-bottom: 5px;
7230}
7231.selectContainer {
7232 max-height: 0;
7233 overflow: hidden;
7234 transition: all .3s ease-out;
7235 position: absolute;
7236 width: calc(100% - 20px);
7237 background: #f2f2f2;
7238 z-index: 1000;
7239 padding: 0 9px 0;
7240 border-radius: 3px;
7241 margin-left: -9px;
7242}
7243.select.opened .selectContainer {
7244 max-height: 800px;
7245 overflow-y: auto;
7246 padding: 0 10px 9px 9px;
7247}
7248.selectContainer .buttDiv {
7249 display: block;
7250 width: 100%;
7251 margin: 0;
7252}
7253.selectContainer .btn {
7254 width: 100%;
7255 margin: 0;
7256 max-height: initial;
7257}
7258.selectContainer .btn a {
7259 padding: 2px 24px 3px 14px;
7260 margin-top: -1px;
7261 display: block;
7262 width: min-content;
7263 min-width: calc(100% - 38px);
7264 white-space: pre-line;
7265 max-height: initial;
7266 text-align: left;
7267}
7268.selectContainer .btn i {
7269 top: calc(50% - 13px);
7270 left: initial;
7271 right: -1px;
7272}
7273.selectContainer .btn .hotkeyHint {
7274 right: initial;
7275 left: 3px;
7276 top: 1px;
7277}
7278.selectHead {
7279 border: 1px solid #d9d9d9;
7280 border-radius: 3px;
7281 height: 26px;
7282 display: flex;
7283 background: #FFFBEA;
7284}
7285.selectDisplay {
7286 flex-grow: 10;
7287 overflow: hidden;
7288 cursor: pointer;
7289}
7290.selectButtons i {
7291 width: 26px;
7292 height: 26px;
7293}
7294.selectButtons .empty {
7295 display: none;
7296}
7297.selectButtons .arrow svg {
7298 height: 30px;
7299 top: -4px;
7300 position: relative;
7301}
7302.selectButtons i {
7303 width: 26px;
7304 height: 26px;
7305 opacity: 0.5;
7306 cursor: pointer;
7307 transition: all .2s ease-out;
7308}
7309.selectButtons i:hover {
7310 opacity: 1;
7311}
7312.selectText {
7313 width: 2000px;
7314 padding: 4px 8px;
7315}
7316.darkSide .selectText {
7317 color: #23272b;
7318}
7319.darkSide .select .selectHead, .darkSide .blockTitle .select .selectHead {
7320 background: #E0DAC0;
7321 border: 1px solid #E0DAC0;
7322}
7323.darkSide .select .selectContainer {
7324 padding: 0 4px 0 2px;
7325 left: 16px;
7326}
7327.darkSide .select.opened .selectContainer {
7328 padding: 3px 4px 2px 2px;
7329}
7330.select:not(.opened) .ffIcon.arrow {
7331 transform: rotate(90deg);
7332}
7333.select.opened .ffIcon.arrow {
7334 transform: rotate(0deg);
7335}
7336.blockTitle .select .selectHead {
7337 background-color: #e6e6e6;
7338}
7339.blockTitle .select {
7340 padding: 0;
7341 margin: 0;
7342}
7343.blockTitle .select .selectContainer {
7344 padding: 0px 4px 0px 2px;
7345 left: 8px;
7346 top: 29px;
7347 width: calc(100% - 4px);
7348}
7349.blockTitle .select.opened .selectContainer {
7350 padding: 2px 4px 2px 2px;
7351}
7352.blockTitle .select .selectText {
7353 padding-top: 6px;
7354 opacity: 0.8;
7355}
7356.blockTitle .select.focused .selectHead {
7357 background-color: #d9d9d9;
7358}
7359.darkSide .blockTitle .select.focused .selectHead {
7360 background-color: #d9d9d9;
7361}
7362.selectHead .popUpTitle {
7363 top: 60px;
7364 left: 9px;
7365}
7366.selectContainer .btn .popUpTitle {
7367 max-width: calc(100% - 20px);
7368 top: -2000px;
7369 visibility: hidden;
7370}
7371.selectContainer .btn:hover .popUpTitle {
7372 top: 20px;
7373 display: block;
7374 visibility: visible;
7375}
7376/* Загрузчик файлов */
7377.fileField .fcFileLine {
7378 display: flex;
7379}
7380.fileField .fcFileLine div {
7381 font-weight: 600;
7382 padding: 0;
7383}
7384.fileField .fcFileName {
7385 display: flex;
7386 align-items: center;
7387 margin-right: 10px !important;
7388 margin-left: 10px;
7389}
7390.fileField .fcFileLine.uploading .fcFileName {
7391 opacity: .9;
7392 animation: blink 1.2s linear infinite;
7393}
7394.fileField .fcontainer.somethingIsUploading .btn.uploadButtIcon, .fileField .fcontainer.somethingIsUploading .btn.uploadButtIcon a {
7395 opacity: 0.7;
7396 left: 0px;
7397 top: 0px;
7398 cursor: wait;
7399}
7400.fileField .btn a {
7401 padding-top: 2px;
7402 padding-bottom: 3px;
7403 color: black;
7404}
7405.fileField .fUploader .element {
7406 margin: 0 0 0 15px;
7407 padding: 0;
7408}
7409.fileField .linkButtonIconContainer {
7410 top: 6px;
7411 width: 20px;
7412}
7413.fileField>.stepTitle {
7414 display: flex;
7415 justify-content: flex-start;
7416 flex-wrap: wrap;
7417 align-items: center;
7418}
7419.fileField>.stepTitle .btn {
7420 margin: 0 0 4px;
7421}
7422.fileField .errorMsgBlock {
7423 bottom: 45px;
7424}
7425/* Модальные окна */
7426.window {
7427 position: fixed;
7428 left: 0;
7429 top: 0;
7430 width: 100vw;
7431 height: 100vh;
7432 max-width: initial;
7433 max-height: initial;
7434 z-index: 1002;
7435 margin: 0;
7436 padding: 0;
7437 background: #0008;
7438 display: none;
7439 justify-content: center;
7440 align-items: center;
7441}
7442.window.opened {
7443 display: flex;
7444}
7445.windowButtons>div {
7446 text-align: right;
7447 padding-right: 0;
7448 padding-bottom: 0;
7449 margin-bottom: -4px;
7450}
7451.windowContent {
7452 padding: 1px;
7453 max-height: calc(100vh - 115px);
7454 overflow: auto;
7455}
7456.windowBox {
7457 margin: 0;
7458}
7459.window.large .windowBox {
7460 flex-grow: 1;
7461}
7462.window:not(.hasTitle) .blockTitle {
7463 display: none;
7464}
7465.window:not(.hasTitle) .windowBox {
7466 padding-top: 10px;
7467}
7468.window:not(.large) .errorMsgBlock {
7469 position: relative;
7470 top: -7px;
7471}
7472.windowContent .element.withSomeError {
7473 margin-top: 23px;
7474}
7475
7476/* Форма поиска по глоссарию */
7477.glossarySearchForm {
7478 display: none !important;
7479 position: absolute;
7480 right: 1px;
7481 width: 400px;
7482 top: 32px;
7483 max-width: 80vw;
7484 background: #4a4e52;
7485 padding: 3px 5px 3px 26px;
7486 border-radius: 3px;
7487}
7488.btnGlossarySearch.active .glossarySearchForm {
7489 display: block !important;
7490}
7491.glossarySearchForm .gloss_button {
7492 display: none;
7493}
7494.glossarySearchForm .gloss_container .input_clear {
7495 top: 3px !important;
7496 font-size: 32px !important;
7497 left: -26px !important;
7498 width: 18px;
7499}
7500.glossarySearchForm .gloss_container .input_bordering {
7501 border: 1px solid #81c45600 !important;
7502 border-radius: 4px;
7503}
7504.glossarySearchForm .gloss_container .input_bordering input {
7505 margin: 0;
7506 font-size: 15px !important;
7507 width: 100% !important;
7508 padding-right: 8px !important;
7509}
7510.glossarySearchForm .gloss_container {
7511 visibility: visible !important;
7512 height: 30px;
7513 display: block;
7514 margin-bottom: -3px;
7515 width: 100% !important;
7516}
7517.darkSide .glossarySearchForm .gloss_container {
7518 margin-bottom: -1px;
7519}
7520
7521/* Виды (view) */
7522.view.hidden {
7523 position: absolute;
7524 z-index: -1;
7525 top: 0;
7526 visibility: hidden;
7527}
7528.view.hidden.excluded {
7529 display: none;
7530}
7531
7532/* actionButton */
7533.btn.actionButtonNoIcon a {
7534 padding-left: 12px;
7535}
7536.actionButtonNoIcon .linkButtonIconContainer {
7537 display: none;
7538}
7539
7540/* Воздушный шар (baloon) */
7541.baloon {
7542 border: 1px solid #81c456;
7543 padding: 10px;
7544 margin-top: 5px;
7545 background: #81c456;
7546 margin-bottom: 0 !important;
7547 border-radius: 5px;
7548}
7549.baloonTitle {
7550 font-size: 15px;
7551 font-weight: 600;
7552 margin: 0 0 5px;
7553}
7554.baloon.full {
7555 width: 100%;
7556}
7557
7558/* flexBox */
7559.flexBox {
7560 display: flex;
7561 justify-content: start;
7562 margin: -10px 0 0 -10px;
7563}
7564.flexBox>div {
7565 margin: 10px 0 0 10px !important;
7566 flex-grow: 10;
7567 min-width: 30%;
7568}
7569@media all and (max-width: 1000px) {
7570 .flexBox {
7571 flex-wrap: wrap;
7572 }
7573}
7574
7575/* infoLine */
7576.infoLine {
7577 line-height: 26px;
7578 display: flex;
7579}
7580.infoLine .ilTitle {
7581 display: inline-block;
7582 margin-right: 8px;
7583 font-size: 15px;
7584 flex-shrink: 0;
7585 max-width: 100%;
7586}
7587.infoLine .ilContent {
7588 font-weight: 600;
7589 white-space: pre-line;
7590 position: relative;
7591 top: 0px;
7592 word-break: break-word;
7593}
7594.infoLine.infoLines {
7595 flex-wrap: wrap;
7596}
7597.infoLine.infoLines .ilContent {
7598 font-weight: 400;
7599 margin-left: 10px;
7600}
7601.infoLine div:not(.infoLine) {
7602 line-height: initial;
7603}
7604
7605/* Спойлеры (Чендлер и Моника поженятся, Лея - сестра Люка, и всё в таком ключе) */
7606.spoiler.element .spoilerHead .btn {
7607 width: 100%;
7608 margin: 0;
7609}
7610.spoiler.element .spoilerHead .btn a {
7611 width: calc(100% - 44px);
7612 text-align: left;
7613 padding: 3px 12px 3px 28px;
7614 margin: 0;
7615}
7616.spoiler.element .spoilerHead .buttDiv {
7617 margin: 0;
7618}
7619.spoiler.element .spoilerHead .hotkeyHint {
7620 top: 4px;
7621 right: 7px;
7622}
7623.spoiler.element .spoilerHead .btn i {
7624 top: 2px;
7625 padding: 0;
7626 left: 4px;
7627 opacity: 0.8 !important;
7628 color: black;
7629}
7630.spoiler.element:not(.opened) .spoilerHead .btn i {
7631 transform: rotate(-90deg);
7632 top: 1px;
7633 left: 5px;
7634}
7635.spoiler.element.opened .spoilerHead .btn i, .spoiler.element:hover .spoilerHead .btn i {
7636 opacity: 1 !important;
7637}
7638.spoiler.element .spoilerContainer {
7639 max-height: 10000px;
7640 transition: all .3s cubic-bezier(1, 0.01, 0.68, 0.4) 0s;
7641}
7642.spoiler.element:not(.opened) .spoilerContainer {
7643 max-height: 0;
7644 overflow: hidden;
7645 transition: all .3s cubic-bezier(0.32, 0.63, 0.01, 1.01) 0s;
7646}
7647.spoiler.element:not(.opened) .spoilerHead .btn a {
7648 background-color: #FFFBEA;
7649}
7650.spoiler.element {
7651 transition: all .3s ease-out;
7652 padding: 8px;
7653 border: 1px solid #fff0;
7654}
7655.spoiler.element .spoilerContent {
7656 padding-top: 10px;
7657 white-space: pre-line;
7658}
7659.spoiler.element.opened {
7660 border: 1px solid #D5D9D3;
7661}
7662.darkSide .spoiler.element.opened {
7663 border-color: #4A4E52;
7664}
7665.darkSide .spoiler.element:not(.opened) .spoilerHead .btn:hover a {
7666 background: #585B5E;
7667}
7668.darkSide .spoiler.element:not(.opened) .spoilerHead .btn a {
7669 background: #E0DAC0;
7670}
7671.darkSide .spoiler.element .spoilerHead .btn:hover i, .darkSide .spoiler.element.opened .spoilerHead .btn i {
7672 color: #FFFBEA;
7673}
7674
7675/* Полноэкранный режим */
7676.fullScreenMode .task {
7677 padding: 0;
7678 overflow: hidden !important;
7679 margin: 0;
7680}
7681.fullScreenMode .task > *:not(.window):not(.customContextMenu) {
7682 /* Забыл уже, зщачем это.
7683 display: none;*/
7684}
7685.fullScreenMode .block.main {
7686 height: calc(100vh - 32px);
7687 width: 100%;
7688 margin: 0;
7689 padding: 0;
7690 border: 0 none;
7691 border-radius: 0px;
7692 display: block;
7693 margin-top: 32px;
7694 max-width: 100%;
7695 background: #f2f2f2;
7696}
7697.fullScreenMode .block.main .block {
7698 margin: 20px 10px 35px !important;
7699}
7700.fullScreenMode .block:not(.windowBox) {
7701 max-width: initial;
7702}
7703.darkSide.fullScreenMode .block.main {
7704 background: #090D12;
7705}
7706.fullScreenMode .block.main > .blockContent {
7707 overflow-x: hidden;
7708 height: 100%;
7709}
7710.fullScreenMode .block.main>.blockContent>.blockButtons, .fullScreenMode .block.main>.blockContent>.blockTitle {
7711 position: fixed;
7712 top: 0px;
7713 background: white;
7714 display: none;
7715}
7716.fullScreenMode .task_focused .block.main, .fullScreenMode .task_focused .block.main>.blockContent>.blockButtons, .fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7717 display: flex;
7718}
7719.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7720 box-shadow: 0 0 5px 0 #0005;
7721}
7722.fullScreenMode .block.main>.blockContent>.blockTitle {
7723 width: calc(100vw - 20px);
7724 margin: 0;
7725 border: 0;
7726 border-radius: 0;
7727 z-index: 1000;
7728 height: 32px;
7729 align-items: center;
7730 padding: 0 10px;
7731}
7732.fullScreenMode .block.main>.blockContent>.blockButtons {
7733 z-index: 1001;
7734 right: 10px;
7735 top: 2px;
7736}
7737.darkSide.fullScreenMode .block.main>.blockContent>.blockButtons, .darkSide.fullScreenMode .block.main>.blockContent>.blockTitle {
7738 color: #DCD8C7;
7739 background: #23272B;
7740}
7741.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7742 box-shadow: 0 0 5px 0 #0005;
7743}
7744.darkSide.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7745 box-shadow: 0 0 5px 0 #DCD8C750;
7746}
7747.fullScreenMode .task__error {
7748 top: 33px;
7749}
7750
7751/* Контекстное меню */
7752.customContextMenu {
7753 position: fixed;
7754 z-index: 9000;
7755}
7756.customContextMenuItemsContainer {
7757 border-radius: 5px;
7758 padding: 1px 2px 1px 1px;
7759 background: #808080;
7760 box-shadow: 3px 3px 18px 2px #0004;
7761}
7762.customContextMenuActionButtons {
7763 padding: 0;
7764 margin: 0;
7765}
7766.customContextMenuActionButtons .buttDiv {
7767 margin-right: 15px;
7768}
7769.customContextMenuActionButtons .btn {
7770 width: 100%;
7771 margin: 0px;
7772}
7773.customContextMenuActionButtons .btn a {
7774 width: 100%;
7775 margin: 1px;
7776 padding: 2px 7px;
7777 border: 0;
7778 border-radius: 0;
7779}
7780.customContextMenuRightClickField {
7781 border: 1px dashed #fffbea;
7782 border-radius: 5px 5px 0 0;
7783 margin: 1px 0 1px 1px;
7784 color: #fffbea;
7785 cursor: pointer;
7786 padding: 2px 7px;
7787}
7788.customContextMenuActionButtons .hotkeyHint {
7789 right: -13px;
7790 top: 0px;
7791}
7792
7793/* Мелочёвка и переопределения */
7794.leftPopup .popUpTitle {
7795 left: initial;
7796 right: 0;
7797}
7798
7799/* Мобильно-специфичные стили */
7800
7801@media all and (max-width: 768px) {
7802 .task {
7803 padding: 25px 0 10px 0;
7804 overflow: hidden;
7805 }
7806 .task:first-child {
7807 padding-top: 0;
7808 }
7809 .block {
7810 border-left: none !important;
7811 border-right: none !important;
7812 border-radius: 0 !important;
7813 padding: 10px;
7814 margin-top: 0;
7815 }
7816 .blockTitle {
7817 padding: 15px 12px !important;
7818 margin: 0;
7819 top: -12px;
7820 width: 100%;
7821 left: -10px;
7822 border-left: none !important;
7823 border-right: none !important;
7824 border-radius: 0 !important;
7825 position: relative;
7826 }
7827 .tabList {
7828 max-width: 96vw;
7829 left: 7vw;
7830 }
7831 textarea {
7832 min-height: 100px !important;
7833 }
7834 .element.focused:not(.imageField), .darkSide .element.focused:not(.imageField) {
7835 background: none;
7836 }
7837 .buttDiv {
7838 margin: 0 10px 10px 0;
7839 padding: 0;
7840 }
7841 .btn {
7842 margin: 0;
7843 }
7844 .btn.active {
7845 margin: 0 !important;
7846 }
7847 .btn a {
7848 min-height: 19px;
7849 margin: 0;
7850 padding: 7px 15px 6px;
7851 vertical-align: baseline !important;
7852 }
7853 .btn.linkButton a {
7854 padding-left: 30px;
7855 }
7856 .btn.linkButton.actionButtonNoIcon a {
7857 padding-left: 16px;
7858 }
7859 .linkButtonIconContainer {
7860 top: 6px;
7861 }
7862 .popUpTitle {
7863 display: none !important;
7864 }
7865 .hotkeyHint {
7866 display: none;
7867 }
7868 .switchButton>i {
7869 top: 6px;
7870 }
7871 .stepTitle.title {
7872 margin: 0 0 10px 0;
7873 }
7874 input {
7875 min-height: 42px;
7876 }
7877 .btn:hover a, .darkSide .btn:hover a {
7878 box-shadow: none;
7879 }
7880 .blockContent>.buttDiv {
7881 margin-left: 9px;
7882 }
7883 .blockButtons .blockTitle {
7884 padding: 2px 10px !important;
7885 margin: 0 !important;
7886 }
7887 .blockButtons {
7888 position: relative;
7889 top: -11px;
7890 width: 100vw;
7891 left: -10px;
7892 flex-wrap: wrap;
7893 }
7894 .blockButtons .blockTitle.tabScroller {
7895 width: max-content;
7896 text-align: right;
7897 padding-top: 7px !important;
7898 flex-grow: 10;
7899 }
7900 .blockButtons .blockTitle.custom {
7901 order: 10;
7902 border-top: 0 none;
7903 padding: 5px 12px !important;
7904 width: 100%;
7905 }
7906 .glossarySearchForm {
7907 width: 100vw;
7908 position: fixed;
7909 left: 0;
7910 max-width: 100vw;
7911 }
7912 .glossarySearchForm .gloss_container .input_bordering {
7913 border-radius: 0;
7914 border-left: 0 !important;
7915 border-right: 0 !important;
7916 box-shadow: -2px 3px 9px 0px #0008;
7917 }
7918 .imageField .btn.linkButton a {
7919 padding-left: 12px;
7920 padding-top: 3px;
7921 height: 19px;
7922 }
7923 .checkboxGroup:not(.asButtons) .btn a:before, .radioGroup:not(.asButtons) .btn a:before {
7924 border: 1px solid #d9d9d9;
7925 }
7926 .errorMsgBlock:after {
7927 border-color:transparent;
7928 border-top-color: rgb(250, 112, 103);
7929 }
7930}
7931
7932";
7933
7934
7935
7936
7937
7938
7939
7940$('head').append('<meta name="referrer" content="no-referrer" />');
7941
7942document.ready = new Promise(resolve => {
7943
7944 if (document.readyState === 'complete') {
7945 resolve();
7946 } else {
7947 function onReady() {
7948 resolve();
7949 document.removeEventListener('DOMContentLoaded', onReady, true);
7950 window.removeEventListener('load', onReady, true);
7951 }
7952
7953 document.addEventListener('DOMContentLoaded', onReady, true);
7954 window.addEventListener('load', onReady, true);
7955 }
7956});
7957
7958function extend(ParentClass, constructorFunction, prototypeHash) {
7959 constructorFunction = constructorFunction || function () {
7960 };
7961 prototypeHash = prototypeHash || {};
7962 if (ParentClass) {
7963 constructorFunction.prototype = Object.create(ParentClass.prototype);
7964 }
7965 for (const i in prototypeHash) {
7966 constructorFunction.prototype[i] = prototypeHash[i];
7967 }
7968 return constructorFunction;
7969}