· 6 years ago · Mar 29, 2020, 10:02 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
3664 if (window.customTitleElements) {
3665 for (const e of customTitleElements) {
3666 blockButtons += "";
3667 }
3668 }
3669
3670 blockButtons += "";
3671 }
3672
3673 let title = '';
3674 if (options.hash.title) {
3675 title = '';
3676 }
3677
3678 // свернуть блок, если блоку назначен класс minimized
3679 let minimized = '';
3680 if (options.hash.class.includes('minimized')) {
3681 minimized = 'minimized';
3682 options.hash.class = options.hash.class.replace('minimized', '');
3683 }
3684
3685 //Подсказка
3686 let help = '';
3687 if (options.hash.help) {
3688 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>";
3689 }
3690
3691 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>";
3692 },
3693 //Рисует баннер для шаблонов Директа
3694 drawBanner: function (banner) {
3695 if (!banner) {
3696 return;
3697 }
3698 const b = banner.data[0];
3699 if (!b) {
3700 return;
3701 }
3702
3703 const title = b.title + (b.title_extension ? ' - ' + b.title_extension : '');
3704 const domain = /https?:\/\/([^/]+)/.exec(b.parametrized_href)[1];
3705
3706 const out =
3707 '<div class="element alien unfocusable bannerElement">' +
3708 '<div class="bannerContainer">' +
3709 '<div class="banner">' +
3710 '<div class="bannerTitle">' +
3711 '<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>' +
3712 '</div>' +
3713 '<div class="bannerText">' + b.body + '</div>' +
3714 '<div class="bannerDomain">' + b.domain + '</div>' +
3715 '</div>' +
3716 '</div>' +
3717 '</div>';
3718
3719 return out;
3720 },
3721 //Рисует дополнительные данные баннера для шаблонов Директа
3722 drawBannerInfo: function (banner) {
3723 if (!banner) {
3724 return;
3725 }
3726 const b = banner.data[0];
3727 const m = banner.meta;
3728 if (!b || !m) {
3729 return;
3730 }
3731
3732 let infoLines = '';
3733
3734 function addInfoLine(title, lineClass, value, valueType, additional) {
3735 let v;
3736
3737 if (!valueType) {
3738 valueType = 'text';
3739 }
3740
3741 switch (valueType) {
3742 case 'link':
3743 v = '<a target="_blank" href="' + additional + '" rel="noreferrer">' + value + '</a>';
3744 break;
3745 case 'somethingOther':
3746 break;
3747 default:
3748 v = value;
3749 }
3750
3751 const line =
3752 '<div class="bannerInfoLine ' + lineClass + '">' +
3753 '<div class="bilTitle">' +
3754 title +
3755 '</div>' +
3756 '<div class="bilValue ' + valueType + '">' +
3757 v +
3758 '</div>' +
3759 '</div>';
3760
3761 infoLines += line;
3762 }
3763
3764 function arrayOrStringToString(something) {
3765 if (Array.isArray(something)) {
3766 let list = '';
3767 for (const item of something) {
3768 list += ', ' + item;
3769 }
3770 list = list.substr(2);
3771 return list;
3772 } else if (something) {
3773 return something;
3774 }
3775 }
3776
3777 //Регионы показа
3778 if (b.geo_country) {
3779 addInfoLine('Регионы показа:', 'geoCountry', arrayOrStringToString(b.geo_country));
3780 }
3781 //ГеоТекст
3782 //if (b.geo_text) {addInfoLine('Регион подробно:', 'geoText', b.geo_text);}
3783 //Языки
3784 if (b.lang) {
3785 addInfoLine('Языки:', 'lang', arrayOrStringToString(b.lang));
3786 }
3787 //Логин
3788 if (b.login) {
3789 addInfoLine('Логин:', 'login', b.login);
3790 }
3791 //Просмотр баннера
3792 if (m.banner_id) {
3793 addInfoLine('Просмотр баннера:', 'bannerLink', m.banner_id, 'link', 'https://direct-mod.yandex-team.ru/history/banner/?bid=' + m.banner_id);
3794 }
3795 //Просмотр кампании
3796 if (m.campaign_id) {
3797 addInfoLine('Просмотр кампании:', 'campaignLink', m.campaign_id, 'link', 'https://direct-mod.yandex-team.ru/campaign/show/?cid=' + m.campaign_id);
3798 }
3799
3800 const out =
3801 '<div class="element alien unfocusable bannerInfoElement">' +
3802 '<div class="bannerInfoContainer">' +
3803 '<div class="bannerInfo">' +
3804 infoLines +
3805 '</div>' +
3806 '</div>' +
3807 '</div>';
3808
3809 return out;
3810 },
3811 misprints: function (misprints) {
3812 if (!misprints) {
3813 return;
3814 }
3815 const stringifiedMisprints =
3816 '<div class="stringifiedMisprints" style="display: none;">' +
3817 JSON.stringify(misprints) +
3818 '</div>';
3819 return stringifiedMisprints;
3820 }
3821};
3822
3823
3824
3825
3826
3827
3828
3829/*
3830*
3831* Всякие функции
3832*
3833*/
3834window.dumpObjectKeysToDocumentBody = (obj) => {
3835 document.querySelector('body').innerHTML+='<br><br>';
3836 for (const k in obj) {
3837 document.querySelector('body').innerHTML+= k + '<br>';
3838 }
3839};
3840
3841window.loadStuffFromTheOneLibrary = ( assignment ) => {
3842 //Вытаскиваем настройки шаблона из объекта
3843 for (const s in window.theOneSettings) {
3844 window[s] = window.theOneSettings[s];
3845 }
3846 //Обогащаем таск и тасксьют методами из библиотеки и накидываем поверх кастомных методов, ежели таковые имеются
3847 const taskSuite = assignment.getTaskSuite();
3848 injectMethods(theOneAssignmentMethods, assignment, window.customAssignmentMethods);
3849 injectMethods(theOneTaskSuitePrototype, taskSuite, window.customTaskSuiteMethods);
3850 for (const task of taskSuite.getTasks()) {
3851 injectMethods(theOneTaskPrototype, task, window.customTaskMethods);
3852 }
3853 if (!window.helpersRegisteredAndCssApplied) {
3854 //Регистрируем библиотечные хелперы Handlebars
3855 for (const name in theOneHandlebarsHelpers) {
3856 Handlebars.registerHelper(name, theOneHandlebarsHelpers[name]);
3857 }
3858 //Вставляем CSS
3859 applyTheOneCSS();
3860 window.helpersRegisteredAndCssApplied = true;
3861 }
3862 //То, что раньше было в onResume()
3863 window.lang = assignment.getWorkspaceOptions().language || 'ru';
3864 //Инициализация жалобщика
3865 if (useComplainter) {
3866 $.getScript(complainterLibSrc, function (data, textStatus, jqxhr) {
3867 let options = assignment.getWorkspaceOptions();
3868 let namespace = complainterNamespace;
3869
3870 window.Complaints.default.initialize({
3871 api: {
3872 url: '',
3873 origin: options.origin,
3874 proxyName: complainterProxyName,
3875 assignmentId: assignment.getId()
3876 },
3877 language: options.language,
3878 namespace,
3879 ajax: function (ajaxOptions) {
3880 !(ajaxOptions.type) && (ajaxOptions.type = ajaxOptions.method);
3881 return assignment.getSandboxChannel().request('proxy', ajaxOptions);
3882 },
3883
3884 custom_causes: complainterCauses,
3885
3886 custom_translations: complainterTranslations,
3887 });
3888 });
3889 }
3890 //Пытаемся выпытать у браузера сохранённую солюшку, если она была
3891 assignment.getTaskSuite().getSolutionsFromStorage();
3892};
3893
3894window.injectMethods = (from, to, customMethods) => {
3895 for (const m in from) {
3896 to[m] = from[m];
3897 }
3898 if (customMethods) {
3899 for (const m in customMethods) {
3900 if (to[m]) {
3901 to['_' + m] = to[m];
3902 }
3903 to[m] = customMethods[m];
3904 }
3905 }
3906};
3907
3908window.applyTheOneCSS = () => {
3909 const style = document.createElement('style');
3910 style.setAttribute('type', 'text/css');
3911 style.innerHTML = theOneCSS;
3912 document.querySelector('head').appendChild(style);
3913};
3914
3915window.initHelpContainer = (q) => {
3916 const popUpTitle = q.querySelector('.popUpTitle');
3917 popUpTitle.innerHTML = "<div class="popUpTitleInner">${popUpTitle.innerHTML}</div>";
3918
3919 q.addEventListener('click', function () {
3920 if (this.classList.contains('triggered')) {
3921 this.classList.remove('triggered');
3922 } else {
3923 this.classList.add('triggered');
3924 makeSureThisDivFitsOnScreen(q.querySelector('.popUpTitle'));
3925 }
3926 });
3927 q.classList.add('initialized');
3928};
3929
3930//Проверяет, помещается ли указанный контейнер на экране, и если нет - сдвигает его и масштабирует
3931window.makeSureThisDivFitsOnScreen = (div) => {
3932 div.setAttribute('style', '');
3933
3934 const
3935 style = getComputedStyle(div),
3936 margin = 20, //Ровно на столько пикселей от края экрана будут отодвигаться подсказки
3937 task = fullScreenMode ? div.closest('.block.main') || div.closest('.windowContent') || div.closest('.task') : div.closest('.windowContent') || div.closest('.task');
3938 let
3939 verticalProperty,
3940 n = 0;
3941
3942
3943 do {
3944 if (n++ > 20) {break;}
3945
3946
3947 let
3948 divRect = div.getBoundingClientRect(),
3949 taskRect = task.getBoundingClientRect(),
3950 divEnd;
3951
3952 //console.log(divRect);
3953 //console.log(taskRect);
3954
3955 //Ширина блока должна быть меньше ширины таска
3956 if (divRect.width > taskRect.width - 2*margin) {
3957 div.style.maxWidth = taskRect.width - 2*margin + "px";
3958 divRect = div.getBoundingClientRect();
3959 taskRect = task.getBoundingClientRect();
3960
3961 }
3962
3963 //Высота блока должна быть меньше высоты таска
3964 if (divRect.height > taskRect.height - 2*margin) {
3965 div.style.maxHeight = taskRect.height - 2*margin + "px";
3966 divRect = div.getBoundingClientRect();
3967 taskRect = task.getBoundingClientRect();
3968 }
3969
3970 //Выход за правую границу
3971 divEnd = divRect.right + margin;
3972 if (divEnd > taskRect.right - 20) {
3973 div.style.left = "-" + (divEnd - taskRect.right).toString() + "px";
3974 divRect = div.getBoundingClientRect();
3975 taskRect = task.getBoundingClientRect();
3976 }
3977
3978 //Выход за нижнюю границу
3979 divEnd = divRect.bottom + margin;
3980 if (!div.classList.contains('errorMsgBlock') && divEnd > taskRect.bottom+10) {
3981 div.style.top = "-" + (divEnd - taskRect.bottom - parseInt(style.top)).toString() + "px";
3982 divRect = div.getBoundingClientRect();
3983 taskRect = task.getBoundingClientRect();
3984 }
3985
3986 //Выход за левую границу
3987 divEnd = divRect.left - margin;
3988 if (divEnd < -10) {
3989 div.style.left = margin + "px";
3990 divRect = div.getBoundingClientRect();
3991 taskRect = task.getBoundingClientRect();
3992 }
3993
3994 //Выход за верхнюю границу
3995 divEnd = divRect.top - margin;
3996 if (!div.classList.contains('errorMsgBlock') && divEnd < taskRect.top-10) {
3997 if (div.classList.contains('errorMsgBlock')) {
3998 div.style.maxHeight = divRect.height + divEnd - taskRect.top + "px";
3999 } else {
4000 div.style.top = "-" + (divEnd - taskRect.top - parseInt(style.top)).toString() + "px";
4001 }
4002 divRect = div.getBoundingClientRect();
4003 taskRect = task.getBoundingClientRect();
4004 }
4005
4006 } while (n<3);
4007};
4008
4009window.initImageField = (imageField) => {
4010 /*
4011 Тут 2 канваса, внешний и внутренний.
4012 img - внутренний канвас с картинкой pic
4013 canvas - внешний канвас.
4014 */
4015 let imageSource = imageField.querySelector('.imageSource'),
4016 imageOrigLink = imageField.querySelector('.imageOriginalLink'),
4017 imageBlock = imageField.querySelector('.imageBlock'),
4018 imageConsole = imageField.querySelector('.imageConsole'),
4019 noCanvasMode = imageField.classList.contains('noCanvas'),
4020 imgRatio, canvasRatio, imgD, leftButtsJSON;
4021 const
4022 leftButts = imageField.querySelector('.imageLeftButts'),
4023 rightButts = imageField.querySelector('.imageRightButts');
4024
4025
4026 if (!imageSource) {
4027 ce('imageField_no_imageSource_div', imageField.className);
4028 return;
4029 }
4030 imageSource = imageSource.innerHTML;
4031 if (!imageSource) {
4032 ce('imageField_no_imageSource', imageField.className);
4033 return;
4034 }
4035
4036 if (imageOrigLink) {
4037 imageOrigLink = imageOrigLink.innerHTML;
4038 }
4039
4040
4041 if (noCanvasMode) {
4042 let img = document.createElement('img');
4043 img.className = 'justAnImage';
4044 img.setAttribute('src', imageSource);
4045 imageBlock.appendChild(img);
4046
4047 leftButtsJSON = {
4048 type: "actionButtonGroup",
4049 class: "imageFieldRightControls",
4050 flags: ["unfocusable"],
4051 buttons: [
4052 {
4053 iconClass: "expandButtIcon",
4054 hint: "Развернуть на весь экран.\nДвойной клик по изображению делает то же самое."
4055 }
4056 ]
4057 };
4058
4059 leftButts.innerHTML = handleFieldAndGetHTML(leftButtsJSON);
4060
4061 let btnA;
4062 btnA = leftButts.querySelector('.expandButtIcon a');
4063 if (btnA) {
4064 let _btnA = btnA;
4065 btnA.addEventListener('click', expandStuff);
4066 imageBlock.addEventListener('dblclick', expandStuff);
4067
4068 function expandStuff(e) {
4069 e.preventDefault();
4070 imageField.classList.toggle('expanded');
4071 const btn = _btnA.closest('.btn');
4072 if (imageField.classList.contains('expanded')) {
4073 btn.classList.add('selected');
4074 } else {
4075 btn.classList.remove('selected');
4076 }
4077 }
4078 }
4079 } else {
4080 const canvas = imageBlock.querySelector('canvas');
4081 const ctx = canvas.getContext("2d");
4082
4083 //Внутренний канвас с картинкой
4084 const img = document.createElement('canvas');
4085 const imgCtx = img.getContext("2d");
4086
4087 ctx.imageSmoothingEnabled = false;
4088 ctx.imageSmoothingEnabled = false;
4089
4090 function initCanvas() {
4091 ctx.canvas.width = canvas.offsetWidth;
4092 ctx.canvas.height = canvas.offsetHeight;
4093 ctx.canvas.halfWidth = canvas.offsetWidth / 2;
4094 ctx.canvas.halfHeight = canvas.offsetHeight / 2;
4095 canvas.scale =/* canvas.scale ? canvas.scale :*/ Math.min(canvas.width / img.width, canvas.height / img.height);
4096 canvas.minScale = /*canvas.minScale ? canvas.minScale : */canvas.scale;
4097 img.rotation = img.rotation ? img.rotation : 0;
4098 img.left = img.width / -2;
4099 img.top = img.height / -2;
4100 onScaleChange();
4101 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4102 ctx.drawImage(img, img.left, img.top);
4103 imgRatio = img.width / img.height;
4104 canvasRatio = canvas.width / canvas.height;
4105 if (imgRatio < canvasRatio) {
4106 imgD = 1;
4107 } else {
4108 imgD = -1;
4109 }
4110 if (imgRatio < 1) {
4111 imgD *= -1;
4112 }
4113 }
4114
4115 let pic = new Image();
4116 pic.src = imageSource;
4117 imgLog('Загружаю изображение...');
4118 pic.onload = function () {
4119 imgLog('Изображение загружено!');
4120 const picMaxD = Math.max(pic.width, pic.height);
4121 const picDiagonal = Math.ceil(Math.sqrt(Math.pow(pic.width, 2) + Math.pow(pic.height, 2)));
4122 img.width = pic.width;
4123 img.height = pic.height;
4124 img.halfWidth = img.width / 2;
4125 img.halfHeight = img.height / 2;
4126 img.scale = 1;
4127 pic.left = 0 - pic.width / 2;
4128 pic.top = 0 - pic.height / 2;
4129 imgCtx.setTransform(img.scale, 0, 0, img.scale, img.halfWidth, img.halfHeight);
4130 imgCtx.drawImage(pic, pic.left, pic.top);
4131
4132 initCanvas();
4133 canvas.initCanvas = initCanvas;
4134 window.addEventListener('resize', initCanvas);
4135
4136 let btnA;
4137 btnA = leftButts.querySelector('.expandButtIcon a');
4138 if (btnA) {
4139 let _btnA = btnA;
4140 btnA.addEventListener('click', expandStuff);
4141 imageBlock.addEventListener('dblclick', expandStuff);
4142
4143 function expandStuff(e) {
4144 e.preventDefault();
4145 if (!imageField.classList.contains('expanded')) {
4146
4147 }
4148 const btn = _btnA.closest('.btn');
4149 if (!imageField.classList.contains('expanded')) {
4150 imageBlock.widthBeforeExpanding = getComputedStyle(imageBlock).width;
4151 imageBlock.heightBeforeExpanding = getComputedStyle(imageBlock).height;
4152 imageField.classList.add('expanded');
4153 btn.classList.add('selected');
4154 initCanvas();
4155 } else {
4156 btn.classList.remove('selected');
4157 imageBlock.style.width = imageBlock.widthBeforeExpanding;
4158 imageBlock.style.height = imageBlock.heightBeforeExpanding;
4159 imageField.classList.remove('expanded');
4160 initCanvas();
4161 setTimeout(() => {
4162 imageBlock.style.width = 'initial';
4163 imageBlock.style.height = 'initial';
4164 }, 100);
4165 }
4166 }
4167 }
4168
4169
4170 //Поворот влево на 90 градусов
4171 btnA = leftButts.querySelector('.undoButtIcon a');
4172 if (btnA) {
4173 btnA.addEventListener('click', function (e) {
4174 e.preventDefault();
4175 img.rotation = Math.ceil(img.rotation * 2) / 2;
4176 img.rotation > 0 ? img.rotation -= 0.5 : img.rotation = 1.5;
4177 canvas.savedScale = canvas.scale * img.scale;
4178 rotatePic();
4179 imgLog("Угол поворота изображения: ${img.rotation * 180}°");
4180 });
4181 }
4182
4183 //Поворот вправо на 90 градусов
4184 btnA = leftButts.querySelector('.redoButtIcon a');
4185 if (btnA) {
4186 btnA.addEventListener('click', function (e) {
4187 e.preventDefault();
4188 img.rotation = Math.floor(img.rotation * 2) / 2;
4189 img.rotation < 2 ? img.rotation += 0.5 : img.rotation = 0.5;
4190 canvas.savedScale = canvas.scale * img.scale;
4191 rotatePic();
4192 imgLog("Угол поворота изображения: ${img.rotation * 180}°");
4193 });
4194 }
4195
4196 //Кнопка свободного вращения
4197 let lastClickedTime;
4198 btnA = leftButts.querySelector('.syncButtIcon a');
4199 if (btnA) {
4200 btnA.addEventListener('click', function (e) {
4201 e.preventDefault();
4202 const btn = this.closest('.btn');
4203 if (!btn.classList.contains('selected')) {
4204 disableButts();
4205 btn.classList.add('selected');
4206 canvas.savedScale = canvas.scale * img.scale;
4207 img.freeRotationMode = true;
4208 } else {
4209 disableButts();
4210 }
4211
4212 //Проверка на двойной клик
4213 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4214 //Это был двойной клик, значит надо сбросить значение данного свойства
4215 img.rotation = 0;
4216 rotatePic();
4217 btn.classList.remove('selected');
4218 img.freeRotationMode = false;
4219 }
4220 //Запоминаем время включения для отслеживания двойного клика
4221 lastClickedTime = new Date().getTime();
4222 });
4223 }
4224
4225 //Инверсия изображения
4226 btnA = leftButts.querySelector('.adjustSolidButtIcon a');
4227 if (btnA) {
4228 btnA.addEventListener('click', function (e) {
4229 e.preventDefault();
4230 const btn = this.closest('.btn');
4231 if (!btn.classList.contains('selected')) {
4232 btn.classList.add('selected');
4233 img.filters.invert = 100;
4234 imgLog("Изображение инвертировано");
4235 } else {
4236 btn.classList.remove('selected');
4237 img.filters.invert = 0;
4238 imgLog("Изображение ревертировано");
4239 }
4240 applyFilters();
4241 });
4242 }
4243
4244 //Оттенок изображения
4245 btnA = leftButts.querySelector('.swatchBookIcon a');
4246 if (btnA) {
4247 btnA.addEventListener('click', function (e) {
4248 e.preventDefault();
4249 const btn = this.closest('.btn');
4250 if (!btn.classList.contains('selected')) {
4251 disableButts();
4252 btn.classList.add('selected');
4253 img.freeHueMode = true;
4254 } else {
4255 disableButts();
4256 }
4257
4258 //Проверка на двойной клик
4259 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4260 //Это был двойной клик, значит надо сбросить значение данного свойства
4261 btn.classList.remove('selected');
4262 img.freeHueMode = false;
4263 img.filters.hue = 0;
4264 applyFilters();
4265 imgLog("Оттенок восстановлен");
4266 }
4267 //Запоминаем время включения для отслеживания двойного клика
4268 lastClickedTime = new Date().getTime();
4269 });
4270 }
4271
4272 //Контрастность изображения
4273 btnA = leftButts.querySelector('.adjustButtIcon a');
4274 if (btnA) {
4275 btnA.addEventListener('click', function (e) {
4276 e.preventDefault();
4277 const btn = this.closest('.btn');
4278 if (!btn.classList.contains('selected')) {
4279 disableButts();
4280 btn.classList.add('selected');
4281 img.freeContrastMode = true;
4282 } else {
4283 disableButts();
4284 }
4285
4286 //Проверка на двойной клик
4287 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4288 //Это был двойной клик, значит надо сбросить значение данного свойства
4289 btn.classList.remove('selected');
4290 img.freeContrastMode = false;
4291 img.filters.contrast = 100;
4292 applyFilters();
4293 imgLog("Контрастность восстановлена");
4294 }
4295 //Запоминаем время включения для отслеживания двойного клика
4296 lastClickedTime = new Date().getTime();
4297 });
4298 }
4299
4300 //Яркость изображения
4301 btnA = leftButts.querySelector('.sunButtIcon a');
4302 if (btnA) {
4303 btnA.addEventListener('click', function (e) {
4304 e.preventDefault();
4305 const btn = this.closest('.btn');
4306 if (!btn.classList.contains('selected')) {
4307 disableButts();
4308 btn.classList.add('selected');
4309 img.freeBrightnessMode = true;
4310 } else {
4311 disableButts();
4312 }
4313
4314 //Проверка на двойной клик
4315 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4316 //Это был двойной клик, значит надо сбросить значение данного свойства
4317 btn.classList.remove('selected');
4318 img.freeBrightnessMode = false;
4319 img.filters.brightness = 100;
4320 applyFilters();
4321 imgLog("Яркость восстановлена");
4322 }
4323 //Запоминаем время включения для отслеживания двойного клика
4324 lastClickedTime = new Date().getTime();
4325 });
4326 }
4327
4328 //Насыщенность изображения
4329 btnA = leftButts.querySelector('.paletteButtIcon a');
4330 if (btnA) {
4331 btnA.addEventListener('click', function (e) {
4332 e.preventDefault();
4333 const btn = this.closest('.btn');
4334 if (!btn.classList.contains('selected')) {
4335 disableButts();
4336 btn.classList.add('selected');
4337 img.freeSaturationMode = true;
4338 } else {
4339 disableButts();
4340 }
4341
4342 //Проверка на двойной клик
4343 if (new Date().getTime() <= lastClickedTime + imageViewerDoubleClickTime) {
4344 //Это был двойной клик, значит надо сбросить значение данного свойства
4345 btn.classList.remove('selected');
4346 img.freeSaturationMode = false;
4347 img.filters.saturation = 100;
4348 applyFilters();
4349 imgLog("Насыщенность восстановлена");
4350 }
4351 //Запоминаем время включения для отслеживания двойного клика
4352 lastClickedTime = new Date().getTime();
4353 });
4354 }
4355
4356 //Стирание фильтров
4357 btnA = leftButts.querySelector('.eraserButtIcon a');
4358 if (btnA) {
4359 btnA.addEventListener('click', function (e) {
4360 e.preventDefault();
4361 disableButts();
4362 img.filters = {
4363 invert: 0,
4364 hue: 0,
4365 contrast: 100,
4366 brightness: 100,
4367 saturation: 100
4368 };
4369 applyFilters();
4370 imgLog("Все фильтры стёрты");
4371 });
4372 }
4373
4374 };
4375
4376 canvas.addEventListener('mousedown', imgDrag);
4377 canvas.addEventListener('mousemove', imgMove);
4378 canvas.addEventListener('mouseup', imgDrop);
4379 canvas.addEventListener('mouseleave', imgDrop);
4380 canvas.addEventListener('wheel', imgWheel);
4381
4382
4383 function disableButts() {
4384 Array.from(leftButts.querySelectorAll('.btn.selected:not(.adjustSolidButtIcon):not(.expandButtIcon)')).forEach(btn => btn.classList.remove('selected'));
4385 ['freeRotationMode', 'freeHueMode', 'freeContrastMode', 'freeBrightnessMode', 'freeSaturationMode'].forEach(name => img[name] = false);
4386 }
4387
4388 function clrscr() {
4389 ctx.clearRect(canvas.left, canvas.top, canvas.scaledWidth, canvas.scaledHeight);
4390 }
4391
4392 function imgClrscr() {
4393 imgCtx.clearRect(-img.halfWidth - 10, -img.halfHeight - 10, img.width + 20, img.height + 20);
4394 }
4395
4396 function imgLog(text) {
4397 imageConsole.style.opacity = "0.8";
4398 imageConsole.innerHTML = text;
4399 clearTimeout(imageConsole.timeout);
4400 imageConsole.timeout = setTimeout(function () {
4401 imageConsole.style.opacity = "0";
4402 }, 2000);
4403 }
4404
4405 img.filters = {
4406 invert: 0,
4407 hue: 0,
4408 contrast: 100,
4409 brightness: 100,
4410 saturation: 100
4411 };
4412
4413 function applyFilters(f) {
4414 if (!f) {
4415 f = img.filters;
4416 }
4417 imgCtx.filter = "invert(${f.invert}%) hue-rotate(${f.hue}deg) contrast(${f.contrast}%) brightness(${f.brightness}%) saturate(${f.saturation}%)";
4418 imgCtx.drawImage(pic, pic.left, pic.top);
4419 ctx.drawImage(img, img.left, img.top);
4420 }
4421
4422
4423 function rotatePic() {
4424 clrscr();
4425 imgClrscr();
4426
4427 let angle = img.rotation * 180 % 180;
4428 angle = angle > 90 ? 90 - (angle - 90) : angle;
4429 let alpha = Math.atan2(img.height, img.width); // Угол диагонали
4430 let alpha_1 = alpha + angle * Math.PI / 180; // Новый угол первой диагонали
4431 let alpha_2 = alpha_1 + Math.PI - 2 * alpha; // Новый угол второй диагонали
4432 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
4433 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
4434
4435
4436 img.scale = 1 / Math.max(W_new / img.width, H_new / img.height);
4437 if (canvas.scale > canvas.minScale || imgD < 0) {
4438 canvas.scale = canvas.savedScale / img.scale;
4439 }
4440
4441 imgCtx.setTransform(img.scale, 0, 0, img.scale, img.halfWidth, img.halfHeight);
4442 imgCtx.rotate(img.rotation * Math.PI);
4443 imgCtx.drawImage(pic, pic.left, pic.top);
4444
4445 onScaleChange();
4446 positionCorrector();
4447 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4448 clrscr();
4449 ctx.drawImage(img, img.left, img.top);
4450 imgLog("Угол поворота изображения: ${Math.round(img.rotation * 180)}°");
4451 }
4452
4453 function imgDrag(e) {
4454 canvas.midnightMover = true;
4455 imageField.classList.add('dragging');
4456 }
4457
4458 function imgMove(e) {
4459 if (!canvas.midnightMover) {
4460 return;
4461 }
4462
4463 img.left += e.movementX / canvas.scale;
4464 img.top += e.movementY / canvas.scale;
4465 positionCorrector();
4466 clrscr();
4467 ctx.drawImage(img, img.left, img.top);
4468
4469 }
4470
4471 function imgDrop() {
4472 canvas.midnightMover = false;
4473 imageField.classList.remove('dragging');
4474 }
4475
4476 function onScaleChange() {
4477 canvas.left = -canvas.halfWidth / canvas.scale;
4478 canvas.top = -canvas.halfHeight / canvas.scale;
4479 canvas.scaledWidth = canvas.width / canvas.scale;
4480 canvas.scaledHeight = canvas.height / canvas.scale;
4481 canvas.right = -canvas.left;
4482 canvas.bottom = -canvas.top;
4483 onPositionChange();
4484 }
4485
4486 function onPositionChange() {
4487 img.right = img.left + img.width;
4488 img.bottom = img.top + img.height;
4489 }
4490
4491 function positionCorrector() {
4492 onPositionChange();
4493 if (img.width > canvas.scaledWidth) {
4494 if (img.right > canvas.right && img.left > canvas.left) {
4495 img.left = canvas.left;
4496 }
4497 if (img.left < canvas.left && img.right < canvas.right) {
4498 img.left = canvas.right - img.width;
4499 }
4500 } else {
4501 if (img.left < canvas.left) {
4502 img.left = canvas.left;
4503 }
4504 if (img.right > canvas.right) {
4505 img.left = canvas.right - img.width;
4506 }
4507 }
4508 if (img.height > canvas.scaledHeight) {
4509 if (img.bottom > canvas.bottom && img.top > canvas.top) {
4510 img.top = canvas.top;
4511 }
4512 if (img.top < canvas.top && img.bottom < canvas.bottom) {
4513 img.top = canvas.bottom - img.height;
4514 }
4515 } else {
4516 if (img.top < canvas.top) {
4517 img.top = canvas.top;
4518 }
4519 if (img.bottom > canvas.bottom) {
4520 img.top = canvas.bottom - img.height;
4521 }
4522 }
4523 onPositionChange();
4524 clrscr();
4525 ctx.drawImage(img, img.left, img.top);
4526 }
4527
4528 function imgWheel(e) {
4529 if (!imageField.classList.contains('focused')) {
4530 return;
4531 }
4532 e.preventDefault();
4533
4534 if (img.freeRotationMode) { //Свободное вращение
4535 if (e.deltaY > 0) {
4536 img.rotation < 2 ? img.rotation += imageViewerRotationStep : img.rotation = imageViewerRotationStep;
4537 } else {
4538 img.rotation > 0 ? img.rotation -= imageViewerRotationStep : img.rotation = 2 - imageViewerRotationStep;
4539 }
4540 rotatePic();
4541 } else if (img.freeHueMode) { //Свободный оттенок
4542 if (e.deltaY < 0) {
4543 img.filters.hue <= 358 ? img.filters.hue += 2 : img.filters.hue = 2;
4544 } else {
4545 img.filters.hue >= 2 ? img.filters.hue -= 2 : img.filters.hue = 358;
4546 }
4547 imgLog("Оттенок повёрнут на ${img.filters.hue}°");
4548 applyFilters();
4549 } else if (img.freeContrastMode) { //Свободная контрастность
4550 if (e.deltaY < 0) {
4551 img.filters.contrast <= 199 ? img.filters.contrast += 1 : img.filters.contrast = 200;
4552 } else {
4553 img.filters.contrast >= 1 ? img.filters.contrast -= 1 : img.filters.contrast = 0;
4554 }
4555 imgLog("Контрастность ${img.filters.contrast}%");
4556 applyFilters();
4557 } else if (img.freeBrightnessMode) { //Свободная яркость
4558 if (e.deltaY < 0) {
4559 img.filters.brightness <= 199 ? img.filters.brightness += 1 : img.filters.brightness = 200;
4560 } else {
4561 img.filters.brightness >= 1 ? img.filters.brightness -= 1 : img.filters.brightness = 0;
4562 }
4563 imgLog("Яркость ${img.filters.brightness}%");
4564 applyFilters();
4565 } else if (img.freeSaturationMode) { //Свободная насыщенность
4566 if (e.deltaY < 0) {
4567 img.filters.saturation <= 199 ? img.filters.saturation += 1 : img.filters.saturation = 200;
4568 } else {
4569 img.filters.saturation >= 1 ? img.filters.saturation -= 1 : img.filters.saturation = 0;
4570 }
4571 imgLog("Насыщенность ${img.filters.saturation}%");
4572 applyFilters();
4573 } else {
4574 const oldScale = canvas.scale;
4575 if (e.deltaY > 0) {
4576 canvas.scale -= imageViewerScalingStep + canvas.scale / 10;
4577 if (canvas.scale < canvas.minScale) {
4578 canvas.scale = canvas.minScale;
4579 }
4580 } else {
4581 canvas.scale += imageViewerScalingStep + canvas.scale / 10;
4582 if (canvas.scale > 4) {
4583 canvas.scale = 4;
4584 }
4585 }
4586 onScaleChange();
4587 positionCorrector();
4588 ctx.setTransform(canvas.scale, 0, 0, canvas.scale, canvas.halfWidth, canvas.halfHeight);
4589 clrscr();
4590 ctx.drawImage(img, img.left, img.top);
4591 }
4592 }
4593
4594
4595 leftButtsJSON = {
4596 type: "actionButtonGroup",
4597 class: "imageFieldRightControls",
4598 flags: ["unfocusable"],
4599 buttons: [
4600 {
4601 iconClass: "expandButtIcon",
4602 hint: "Развернуть на весь экран.\nДвойной клик по изображению делает то же самое."
4603 },
4604 {
4605 iconClass: "undoButtIcon",
4606 hint: "Повернуть изображение против часовой стрелки."
4607 },
4608 {
4609 iconClass: "redoButtIcon",
4610 hint: "Повернуть изображение по часовой стрелке."
4611 },
4612 {
4613 iconClass: "syncButtIcon",
4614 hint: "Режим свободного вращения колёсиком.\nДвойная активация сбрасывает поворот."
4615 }
4616 ]
4617 };
4618
4619 if (!imageField.classList.contains('noFilters')) {
4620 leftButtsJSON.buttons.push(
4621 {
4622 iconClass: "adjustSolidButtIcon",
4623 hint: "Инвертировать изображение"
4624 },
4625 {
4626 iconClass: "swatchBookIcon",
4627 hint: "Оттенок изображения.\nДвойная активация сбрасывает фильтр."
4628 },
4629 {
4630 iconClass: "adjustButtIcon",
4631 hint: "Контрастность изображения.\nДвойная активация сбрасывает фильтр."
4632 },
4633 {
4634 iconClass: "sunButtIcon",
4635 hint: "Яркость изображения.\nДвойная активация сбрасывает фильтр."
4636 },
4637 {
4638 iconClass: "paletteButtIcon",
4639 hint: "Насыщенность изображения.\nДвойная активация сбрасывает фильтр."
4640 },
4641 {
4642 iconClass: "eraserButtIcon",
4643 hint: "Стереть все фильтры."
4644 }
4645 );
4646 }
4647
4648 leftButts.innerHTML = handleFieldAndGetHTML(leftButtsJSON);
4649 }
4650
4651
4652 const rightButtsJSON = {
4653 type: "linkButtonGroup",
4654 class: "imageFieldRightControls",
4655 flags: ["unfocusable"],
4656 buttons: [
4657 {
4658 link: imageOrigLink ? imageOrigLink : imageSource,
4659 hint: "Открыть изображение в новом окне.",
4660 caption: "Просто ссылка"
4661 }
4662 ]
4663 };
4664
4665 if (!imageField.classList.contains('noSearchButtons')) {
4666 rightButtsJSON.buttons.push(
4667 {
4668 type: "yandexSearchByPic",
4669 hint: "Искать похожие картинки в Яндексе.",
4670 query: imageOrigLink ? imageOrigLink : imageSource
4671 }
4672 );
4673 rightButtsJSON.buttons.push(
4674 {
4675 type: "googleSearchByPic",
4676 hint: "Искать похожие картинки в другом поисковике.",
4677 query: imageOrigLink ? imageOrigLink : imageSource
4678 }
4679 );
4680 }
4681
4682 rightButts.innerHTML = handleFieldAndGetHTML(rightButtsJSON);
4683};
4684
4685window.isInteger = (s) => {
4686 return /^\d+$/.test(s);
4687};
4688
4689window.isFloat = (s) => {
4690 return /^\d+(\.\d+)?$/.test(s);
4691};
4692
4693window.isEmail = (s) => {
4694 return /^([a-z\u00a1-\uffff0-9_\-.])+@([a-z\u00a1-\uffff0-9_\-.])+\.([a-z\u00a1-\uffff]{2,6})$/i.test(s);
4695};
4696
4697window.isIP = (s) => {
4698 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);
4699};
4700
4701window.isTelNumber = (n) => {
4702 return /^(\s*)?(\+)?([- _():=+]?\d[- _():=+]?){10,14}(\s*)?$/.test(n);
4703};
4704
4705window.isLink = (zelda) => {
4706 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://';
4707};
4708
4709window.isServiceLink = (zelda, hyrule) => {
4710 switch (hyrule) {
4711 case 'vkontakte':
4712 return /^http(s)?:\/\/(www\.)?vk\.com\/.*$/.test(zelda);
4713 case 'youtube':
4714 return /^http(s)?:\/\/(www\.)?youtu(be)?\.(com)|(be)\/.*$/.test(zelda);
4715 case 'yadisk':
4716 return /^http(s)?:\/\/(www\.)?yadi\.sk\/.*$/.test(zelda);
4717 case 'telegram':
4718 return /^http(s)?:\/\/(www\.)?t\.me\/.*$/.test(zelda);
4719 case 'instagram':
4720 return /^http(s)?:\/\/(www\.)?instagram\.com\/.*$/.test(zelda);
4721 case 'facebook':
4722 return /^http(s)?:\/\/(www\.)?facebook\.com\/.*$/.test(zelda);
4723 case 'wikipedia':
4724 return /^http(s)?:\/\/(www\.)?((ru\.)|(en\.)|(by\.)|(simple\.))?wikipedia\.org\/.*$/.test(zelda);
4725 case 'kinopoisk':
4726 return /^http(s)?:\/\/(www\.)?kinopoisk\.ru\/.*$/.test(zelda);
4727 case 'yamusic':
4728 return /^https:\/\/music.yandex.ru\/album\/\d+\/track\/\d+/.test(zelda);
4729 }
4730};
4731
4732window.isYaDiskLink = (zelda) => {
4733 return zelda.substr(0, 15) === 'http://yadi.sk/' || zelda.substr(0, 16) === 'https://yadi.sk/';
4734};
4735
4736window.isDate = (text, format) => {
4737 switch (format) {
4738 case 'YYYY-MM-DD':
4739 return /^([012]\d{3})-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/.test(text);
4740 case 'DD-MM-YYYY':
4741 return /^(0[1-9]|[12]\d|3[01])-(0[1-9]|1[012])-([012]\d{3})$/.test(text);
4742 case 'MM-DD-YYYY':
4743 return /^(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])-([012]\d{3})$/.test(text);
4744 case 'YYYY.MM.DD':
4745 return /^([012]\d{3})\.(0[1-9]|1[012])\.(0[1-9]|[12]\d|3[01])$/.test(text);
4746 case 'DD.MM.YYYY':
4747 return /^(0[1-9]|[12]\d|3[01])\.(0[1-9]|1[012])\.([012]\d{3})$/.test(text);
4748 case 'MM.DD.YYYY':
4749 return /^(0[1-9]|1[012])\.(0[1-9]|[12]\d|3[01])\.([012]\d{3})$/.test(text);
4750 }
4751 return false;
4752};
4753
4754window.skipProtocolParametersAndOtherStuff = (s) => {
4755 if (!s) {
4756 return s
4757 }
4758 return s.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").replace(/(?:\?.*)?(?:#.*)?$/i, "").replace(/^(youtube\.)(.*)(\/featured|\/about|\/channels|\/community|\/playlists|\/videos)$/i, '$1$2');
4759};
4760
4761window.isOptional = (props) => { //Проверяет, есть ли у поля флаг опциональности (для необязательных полей)
4762 return !!(props && props.flags && props.flags.includes('optional'));
4763};
4764
4765window.isActive = (btn) => {
4766 return btn.classList.contains('active') || btn.dataset.type === 'linkButton';
4767};
4768
4769window.isGroupInfinite = (groupDiv) => {
4770 return groupDiv.classList.contains('infinite');
4771};
4772
4773window.activateButton = (btn) => {
4774 btn.classList.add('active');
4775};
4776
4777window.deactivateButton = (btn) => {
4778 btn.classList.remove('active');
4779};
4780
4781window.invertButton = (btn) => {
4782 btn.classList.remove('clear');
4783 if (isActive(btn)) {
4784 deactivateButton(btn);
4785 } else {
4786 activateButton(btn);
4787 }
4788};
4789
4790window.date2Unix = (date, format) => {
4791 const dateArr = date.split('-');
4792 switch (format) {
4793 case 'YYYY-MM-DD':
4794 return new Date(dateArr[0] + '-' + dateArr[1] + '-' + dateArr[2]).getTime();
4795 case 'DD-MM-YYYY':
4796 return new Date(dateArr[2] + '-' + dateArr[1] + '-' + dateArr[0]).getTime();
4797 case 'MM-DD-YYYY':
4798 return new Date(dateArr[1] + '-' + dateArr[2] + '-' + dateArr[0]).getTime();
4799 case 'YYYY.MM.DD':
4800 return new Date(dateArr[0] + '.' + dateArr[1] + '.' + dateArr[2]).getTime();
4801 case 'DD.MM.YYYY':
4802 return new Date(dateArr[2] + '.' + dateArr[1] + '.' + dateArr[0]).getTime();
4803 case 'MM.DD.YYYY':
4804 return new Date(dateArr[1] + '.' + dateArr[2] + '.' + dateArr[0]).getTime();
4805 }
4806};
4807
4808window.isFirstDateAheadOfSecond = (date1, format1, date2, format2) => {
4809 return date2Unix(date1, format1) > date2Unix(date2, format2);
4810
4811};
4812
4813window.ce = (error, additional) => { //Выводит в консоль ошибку с определённым текстом
4814 console.error((templateErrorsJSON[error] ? templateErrorsJSON[error] : error) + (additional ? ' (' + additional + ')' : ''));
4815};
4816
4817window.cl = () => {
4818 console.log.apply(this, arguments);
4819};
4820
4821window.escapeHTML = (text) => {
4822 const map = {
4823 '&': '&',
4824 '<': '<',
4825 '>': '>',
4826 '"': '"',
4827 "'": '''
4828 };
4829
4830 return text.replace(/[&<>"']/g, function (m) {
4831 return map[m];
4832 });
4833};
4834
4835window.localize = (slug, noConsole) => {
4836 if (!window.l10n) {
4837 if (!noConsole) {console.error('[Вы пытаетесть что-то перевести функцией localize, но window.l10n у вас не определён (например, вызовом Handlebars-хелпера setLocalization).]');}
4838 return;
4839 }
4840 if (!window.l10n[slug]) {
4841 if (!noConsole) {console.error('[В window.l10n нет строк локализации для "' + slug + '"!]');}
4842 return;
4843 }
4844 let localizedString = window.l10n[slug][window.lang];
4845 if (!localizedString && localizedString !== '') {
4846 localizedString = window.l10n[slug].ru;
4847 }
4848 if (!localizedString && localizedString !== '') {
4849 localizedString = window.l10n[slug].en;
4850 }
4851 return localizedString;
4852};
4853
4854window.isDescendant = (parent, child) => {
4855 if (!parent || !child) {
4856 return false;
4857 }
4858 let node = child.parentNode;
4859 while (node != null) {
4860 if (node === parent) {
4861 return true;
4862 }
4863 node = node.parentNode;
4864 }
4865 return false;
4866};
4867
4868window.checkFilesExtensions = (field, extensions) => {
4869 let soWhat = true;
4870 Array.from(field.querySelectorAll('.file__name')).forEach(function (name) {
4871 let result = false;
4872 Array.from(extensions).forEach(function (ext) {
4873 if (new RegExp('\.' + ext + '$', 'i').test(name.innerHTML)) {
4874 result = true;
4875 }
4876 });
4877 if (!result) {
4878 soWhat = false;
4879 name.classList.add('red');
4880 }
4881 });
4882 return soWhat;
4883};
4884
4885window.getInputFieldHTML = (field, propsIndex) => {
4886 let title = '';
4887 if (field.hint) {
4888 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
4889 }
4890 if (field.properties && field.properties.examples) {
4891 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
4892 }
4893
4894 //Подсказка
4895 let help = '';
4896 if (field.titleHelp) {
4897 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>";
4898 }
4899
4900 return '' +
4901 '<div class="element ' +
4902 flagsToClassName(field.flags) +
4903 ' ' + (field.class ? field.class + ' ' : '') +
4904 '">' +
4905 '<div class="elementTitle">' +
4906 '<div class="stepQuestion">' + (field.title ? field.title + (field.properties && field.properties.format ? ' (в формате ' + field.properties.format + ')' : '') + ' ' : '') + help + '</div>' +
4907 '</div>' +
4908 '<div class="elementBody fieldType_' + field.type + ' fieldName_' + field.name + '">' +
4909 '<input class="clear ' +
4910 (field.flags && field.flags.includes('alien') ? 'alien' : '') +
4911 (title ? 'cursorHelp ' : '') +
4912 '" data-type="' + field.type + '" data-name="' + field.name + '" ' +
4913 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
4914 (propsIndex || propsIndex === 0 ? 'data-props="' + propsIndex + '"' : '') +
4915 ' type="text" ' + (field.placeholder ? 'placeholder="' + field.placeholder + '"' : '') +
4916 ' ' + (field.title && !title ? 'title="' + field.title + '"' : 'data-title=\'' + field.title.replace(/'/g, '\"') + '\'') + ' ' +
4917 (field.value ? 'value="' + field.value + '"' : '') + ' ' +
4918 (field.flags && field.flags.includes('disabled') ? 'disabled' : '') +
4919 (field.flags && field.flags.includes('readOnly') ? 'readonly' : '') + '>' +
4920 (title ? '<div class="popUpTitle">' + title + '</div>' : '') +
4921 '</div>' +
4922 '</div>';
4923};
4924
4925window.getTextareaFieldHTML = (field, propsIndex) => {
4926 let title = '';
4927
4928 if (field.hint) {
4929 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
4930 }
4931
4932 if (field.properties && field.properties.examples) {
4933 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
4934 }
4935
4936 //Подсказка
4937 let help = '';
4938 if (field.titleHelp) {
4939 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>";
4940 }
4941
4942 return '' +
4943 '<div class="element ' +
4944 flagsToClassName(field.flags) +
4945 (field.flags && field.flags.includes('selected') ? ' toBeClickedAtStart ' : '') +
4946 ' ' + (field.class ? field.class + ' ' : '') +
4947 '">' +
4948 '<div class="elementTitle">' +
4949 '<div class="stepQuestion">' + (field.title ? field.title + (field.properties && field.properties.format ? ' (в формате ' + field.properties.format + ')' : '') + ' ' : '') + help + '</div>' +
4950 '</div>' +
4951 '<div class="elementBody fieldType_' + field.type + ' fieldName_' + field.name + '">' +
4952 '<textarea class="clear ' +
4953 (field.flags && field.flags.includes('alien') ? ' alien ' : '') +
4954 (title ? 'cursorHelp ' : '') +
4955 '" data-type="' + field.type + '" data-name="' + field.name + '" ' +
4956 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
4957 (propsIndex || propsIndex === 0 ? 'data-props="' + propsIndex + '"' : '') +
4958 ' type="text" ' +
4959 (field.placeholder ? 'placeholder="' + field.placeholder + '"' : '') + ' ' +
4960 (field.title && !title ? 'title="' + field.title + '"' : 'data-title=\'' + field.title.replace(/'/g, '\"') + '\'') + ' ' +
4961 (field.flags && field.flags.includes('disabled') ? 'disabled' : '') +
4962 (field.flags && field.flags.includes('readOnly') ? 'readonly' : '') +
4963 '>' + (field.value ? field.value : '') + '</textarea>' +
4964 (title ? '<div class="popUpTitle"' + title + '</div>' : '') +
4965 '</div>' +
4966 '</div>';
4967};
4968
4969window.getSwitchButtonHTML = (field, propsIndex) => {
4970 let includesString = ' includes';
4971
4972 if (propsIndex && fieldsPropsArray[propsIndex].includes) {
4973 Array.from(fieldsPropsArray[propsIndex].includes).forEach(function (name) {
4974 includesString += ' ' + name;
4975 });
4976 } else {
4977 includesString = '';
4978 }
4979
4980 if (field.flags && field.flags.includes('selected')) {
4981 includesString += ' toBeClickedAtStart';
4982 }
4983
4984 let title = '';
4985 if (field.hint) {
4986 title += '<div class="popUpTitle"><div class="buttPath">Подсказка:</div>' + field.hint + '\n</div>';
4987 includesString += ' hasHint';
4988 }
4989
4990 let additional = '';
4991 if (field.help) {
4992 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>';
4993 includesString += ' hasHelp';
4994 }
4995
4996 if (!field.caption) {
4997 field.caption = field.name;
4998 }
4999
5000 return '<div class="buttDiv fieldName_' +
5001 field.name +
5002 (field.flags && field.flags.includes('inline') ? ' inline' : '') +
5003 '">' +
5004 '<div class="btn switchButton correct' +
5005 includesString +
5006 flagsToClassName(field.flags) +
5007 (propsIndex && fieldsPropsArray[propsIndex].excludes ? ' excludesSomething' : '') +
5008 '" data-name="' +
5009 field.name +
5010 '" data-type="switchButton"' +
5011 (propsIndex || propsIndex === 0 ? ' data-props="' + propsIndex + '"' : '') +
5012 (field.radiogroup ? ' data-radiogroup="' + field.radiogroup + '"' : '') +
5013 (field.select ? ' data-select="' + field.select + '"' : '') +
5014 (field.checkboxgroup ? ' data-checkboxgroup="' + field.checkboxgroup + '"' : '') +
5015 (field.groupSolutionName ? ' data-groupsolutionname="' + field.groupSolutionName + '"' : '') +
5016 '>' +
5017 "<i class="ffIcon" data-icon="${field.icon ? field.icon : 'done'}"></i>" +
5018 '<a>' + field.caption + '</a>' +
5019 title +
5020 '</div>' +
5021 additional +
5022 '</div>';
5023};
5024
5025window.getLinkButtonHTML = (field, propsIndex) => {
5026 field.type = field.service;
5027 if (!field.caption) {field.caption = '';}
5028 let includesString = 'includes';
5029 if (propsIndex && fieldsPropsArray[propsIndex].includes) {
5030 Array.from(fieldsPropsArray[propsIndex].includes).forEach(function (name) {
5031 includesString += ' ' + name;
5032 });
5033 } else {
5034 includesString = '';
5035 }
5036 if (field.flags && field.flags.includes('selected')) {
5037 includesString += ' toBeClickedAtStart';
5038 }
5039 if (field.flags && field.flags.includes('mustBeClicked')) {
5040 includesString += ' mustBeClicked';
5041 }
5042 let title = '';
5043 if (field.hint) {
5044 title += '<div class="buttPath">Подсказка:</div>' + field.hint + '\n';
5045 }
5046 if (field.properties && field.properties.examples) {
5047 title += '<div class="buttPath">Примеры:</div>' + field.properties.examples + '\n';
5048 }
5049
5050 let href, iconClass, caption;
5051 if (!field.type) {
5052 href = field.link;
5053 iconClass = field.iconClass ? field.iconClass : 'defaultButtIcon';
5054 caption = field.caption;
5055 } else if (linkButtonTypes[field.type]) {
5056 href = field.link ? field.link : linkButtonTypes[field.type].link;
5057 iconClass = field.iconClass ? field.iconClass : linkButtonTypes[field.type].iconClass;
5058 caption = field.caption ? field.caption : linkButtonTypes[field.type].caption;
5059 if (field.query) {
5060 href += encodeURIComponent(field.query);
5061 }
5062 if (linkButtonTypes[field.type].linkAfter) {
5063 href += linkButtonTypes[field.type].linkAfter;
5064 } else if (field.linkAfter) {
5065 href += field.linkAfter;
5066 }
5067 } else {
5068 ce('wrong_linkButtonGroup_button_type', 'type:' + field.type + ', caption:' + field.caption);
5069 }
5070
5071 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>';
5072};
5073
5074window.getImageViewerHTML = (field, propsIndex) => {
5075 //Подсказка
5076 let help = '';
5077 if (field.titleHelp) {
5078 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>";
5079 }
5080
5081 let output =
5082 '<div class="element imageField fieldName_' + field.name +
5083 flagsToClassName(field.flags) +
5084 '">' +
5085 (field.title ? '<div class="stepTitle">' + field.title + help +'</div>' : '') +
5086 '<div class="imageHeader">' +
5087 '<div class="imageSource">' + field.link + '</div>' +
5088 (field.origLink ? '<div class="imageOriginalLink">' + field.origLink + '</div>' : '') +
5089 '<div class="imageInfo"></div>' +
5090 '<div class="imageButts"></div>' +
5091 '</div>' +
5092 '<div class="imageBlock">' +
5093 '<canvas></canvas>' +
5094 '<div class="imageConsole"></div>' +
5095 '</div>' +
5096 '<div class="imageFooter">' +
5097 '<div class="imageLeftButts"></div>' +
5098 '<div class="imageRightButts"></div>' +
5099 '</div>' +
5100 '</div>';
5101 return output;
5102};
5103
5104window.getFileFieldHTML = (field, propsIndex) => {
5105 //Подсказка
5106 let help = '';
5107 if (field.titleHelp) {
5108 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>";
5109 }
5110
5111 let output =
5112 "<div class="element fileField fieldName_${field.name} ${flagsToClassName(field.flags)}" ${field.caption ? "data-caption="${field.caption}"" : ''}
5113 ${propsIndex ? "data-props="${propsIndex}"" : ''}
5114 ${field.name ? "data-name="${field.name}"" : ''}
5115 >
5116 <div class="stepTitle title">${field.title || ''} ${help}</div>
5117 <div class="fContainer"></div>
5118 </div>";
5119 return output;
5120};
5121
5122window.flagsToClassName = (flags) => {
5123 if (!flags || !Array.isArray(flags)) {
5124 return '';
5125 }
5126
5127 function maybeRename(flag) {
5128 switch (flag) {
5129 case 'block':
5130 return '';
5131 case 'oldSchool':
5132 return 'asButtons';
5133 case 'focused':
5134 return 'focusMeOnStart';
5135 default:
5136 return flag;
5137 }
5138 }
5139
5140 let s = ' ';
5141 for (const flag of flags) {
5142 s += maybeRename(flag) + ' ';
5143 }
5144 return s;
5145};
5146
5147//Функция получает на вход поле из входной спеки, обрабатывает его и возвращает его HTML код
5148window.handleFieldAndGetHTML = (field, handlebarsContext) => {
5149 //Если у поля не прописаны обязательные поля - забиваем
5150 if (!field.type) {
5151 return;
5152 }
5153 if (!field.title) {
5154 field.title = '';
5155 }
5156
5157 if (!handlebarsContext) {
5158 if (this && this.getTask) {
5159 handlebarsContext = this.getTask().input_values;
5160 } else {
5161 handlebarsContext = window.handlebarsContext;
5162 }
5163 }
5164
5165 //Проверка полей на операторы, плейсхолдеры и т.д.
5166 for (const p in field) {
5167 if (typeof (field[p]) != "string") {
5168 continue;
5169 }
5170 const split = field[p].split('|||');
5171 if (split[1]) { //Похоже, что поле с оператором
5172 switch (split[0]) {
5173 case 'INPUT': //В сплит3 вместо ▼ надо будет подставить значение входного поля сплит2
5174 const input = window.currentRenderingTask.getTask().input_values;
5175 if (!split[1] || !split[2]) {
5176 ce('output_placeholder_input_insufficient_parameters', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5177 break
5178 }
5179 if (!input[split[1]] || (typeof (input[split[1]]) === "string" && input[split[1]].length === 0)) {
5180 if (field.default && p !== 'default') {
5181 field[p] = field.default;
5182 } else {
5183 return '';
5184 }
5185 } else {
5186 field[p] = split[2].replace('▼', input[split[1]]).replace('▼', input[split[1]]);
5187 }
5188 break;
5189 case 'somethingOther':
5190 break;
5191 default:
5192 ce('output_placeholder_not_found', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5193 break;
5194 }
5195 }
5196 }
5197
5198 //Локализация [v1.18+]
5199 for (const p in field) {
5200 if (typeof field[p] == 'object' && (field[p].ru || field[p].en)) {
5201 field[p] = field[p][window.lang];
5202 }
5203 }
5204
5205 //Компилируем Handlebars в свойствах поля
5206 function compileStringsHere(where) {
5207 for (const p in where) {
5208 if (!where[p]) {
5209 //continue;
5210 } else
5211 if (typeof where[p] === 'string') {
5212 where[p] = Handlebars.compile(where[p])(handlebarsContext);
5213 } else if (typeof where[p] === 'object'){
5214 compileStringsHere(where[p]);
5215 }
5216 /*if (Array.isArray(where[p])) {
5217 for (const e in where[p]) {
5218 where[p][e] = Handlebars.compile(where[p][e])(handlebarsContext);
5219 }
5220 } else
5221 if (where[p] instanceof Object) {
5222 compileStringsHere(where[p]);
5223 }*/
5224 }
5225 }
5226 if (handlebarsContext) {
5227 compileStringsHere(field);
5228 }
5229
5230 //Если у поля есть массив properties - нам надо добавить его в fieldsPropsArray и в dataset поля добавить ссылку, т.к. потом, очевидно, пригодятся
5231 let propsIndex = null;
5232 if (field.properties) {
5233 propsIndex = fieldsPropsArray.length;
5234 fieldsPropsArray.push(field.properties);
5235 }
5236
5237
5238 //Определимся с типом поля, т.к. от этого будут зависеть наши дальнейшие действия
5239 let groupHTML, help;
5240 switch (field.type) {
5241 case 'date': //Поле со вводом даты. Обычный инпут.
5242 case 'text': //Поле со вводом текста. Обычный инпут.
5243 case 'email': //Поле со вводом мыла. Обычный инпут.
5244 case 'link': //Поле со вводом ссылки. Обычный инпут.
5245 case 'integer': //Поле со вводом целого числа. Обычный инпут.
5246 case 'float': //Поле со вводом дробного числа. Обычный инпут.
5247 return getInputFieldHTML(field, propsIndex);
5248 case 'textarea':
5249 return getTextareaFieldHTML(field, propsIndex);
5250 case 'switchButton':
5251 return getSwitchButtonHTML(field, propsIndex);
5252 case 'linkButton':
5253 if (field.fieldType === 'linkButtonGroup' && !field.service && !field.caption) {
5254 return '';
5255 } //Если надо вывести кнопки только для определённых языков, например
5256 return getLinkButtonHTML(field, propsIndex);
5257 case 'image':
5258 return getImageViewerHTML(field, propsIndex);
5259 case 'file':
5260 return getFileFieldHTML(field, propsIndex);
5261 case 'title':
5262 return '<div class="element unfocusable ' + (field.class ? field.class : '') + '"><div class="elementTitle"><div class="title">' + field.caption + '</div></div></div>';
5263 case 'title2':
5264 return '<div class="element unfocusable ' + (field.class ? field.class : '') + '"><div class="elementTitle"><div class="stepQuestion title">' + field.caption + '</div></div></div>';
5265 case 'label':
5266 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>';
5267 case 'clickableLink':
5268 if (!field.caption) {
5269 field.caption = decodeURIComponent(field.link.replace(/\+/g, ' '));
5270 }
5271 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>';
5272 case 'radioGroup':
5273 case 'checkboxGroup':
5274 case 'buttonGroup':
5275 //Подсказка
5276 help = '';
5277 if (field.titleHelp) {
5278 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>";
5279 }
5280
5281 groupHTML = field.title ? '<div class="stepTitle title">' + field.title + help + '</div>' : '';
5282 Array.from(field.buttons).forEach(function (button) {
5283 if (!field.name && field.type !== 'buttonGroup') {
5284 ce('no_field_name', 'type:' + field.type + ', title:' + field.title);
5285 return;
5286 }
5287
5288 if (!button.name) {
5289 ce('no_button_name', 'field type:' + field.type + ', field title:' + field.title + ', button caption:' + field.caption);
5290 return;
5291 }
5292
5293 button.type = 'switchButton';
5294 button[field.type.toLowerCase()] = field.name;
5295 if (field.solutionName) {button['groupSolutionName'] = field.solutionName;}
5296
5297 if (!button.flags) {
5298 button.flags = [];
5299 }
5300
5301 if (field.flags && field.flags.includes('block')) {
5302 button.flags.push('block');
5303 }
5304
5305 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5306 button.flags.push('inline');
5307 }
5308
5309 groupHTML += handleFieldAndGetHTML(button);
5310 });
5311
5312 if (!field.title) {
5313 field.title = '';
5314 }
5315
5316 return "<div class="${field.type} element fieldName_${field.name} ${field.name} ${flagsToClassName(field.flags)}"
5317 data-name="${field.name}"
5318 ${field.solutionName ? "data-solutionname="${field.solutionName}"" : ''}
5319 data-title='${field.title.replace(/'/g, '\"')}'
5320 >${groupHTML}</div>";
5321 case 'select':
5322 groupHTML = '';
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 if (!button.name) {
5329 ce('no_button_name', 'field type:' + field.type + ', field title:' + field.title + ', button caption:' + field.caption);
5330 return;
5331 }
5332 button.type = 'switchButton';
5333 button[field.type.toLowerCase()] = field.name;
5334 if (field.solutionName) {button['groupSolutionName'] = field.solutionName;}
5335 if (!button.flags) {
5336 button.flags = [];
5337 }
5338 if (field.flags && field.flags.includes('block')) {
5339 button.flags.push('block');
5340 }
5341 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5342 button.flags.push('inline');
5343 }
5344 groupHTML += handleFieldAndGetHTML(button);
5345 });
5346 if (!field.title) {
5347 field.title = '';
5348 }
5349
5350 let includesString, title = '';
5351 if (field.hint) {
5352 title += '<div class="popUpTitle"><div class="buttPath">Подсказка:</div>' + field.hint + '\n</div>';
5353 includesString += ' hasHint';
5354 }
5355
5356 let additional = '';
5357 if (field.help) {
5358 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>';
5359 includesString += ' hasHelp';
5360 }
5361
5362 return '<div class="' + field.type + ' element fieldName_' + field.name + ' ' + field.name + ' ' +
5363 flagsToClassName(field.flags) + includesString +
5364 '" style="' +
5365 (field.width ? " width: ${field.width};" : '') +
5366 '" data-name="' + field.name + '"' +
5367 (field.solutionName ? "data-solutionname="${field.solutionName}"" : '') +
5368 ' data-title=\'' + field.title.replace(/'/g, '\"') + '\'>' +
5369 (field.title ? '<div class="stepTitle title">' + field.title + additional + '</div>' : '') +
5370 '<div class="selectHead">' +
5371 '<div class="selectDisplay">' +
5372 '<div class="selectText">' +
5373 '</div>' +
5374 '</div>' +
5375 '<div class="selectButtons">' +
5376 '<i class="ffIcon empty" data-icon="times"></i>' +
5377 '<i class="ffIcon arrow" data-icon="angle_down"></i>' +
5378 '</div>' + title +
5379 '</div>' +
5380 '<div class="selectContainer">' +
5381 groupHTML +
5382 '</div>'+
5383 '</div>';
5384 case 'group':
5385 //Генерируем HTML всей внутрянки этой группы
5386 groupHTML = '';
5387 Array.from(field.groupFields).forEach(function (groupField) {
5388 groupHTML += handleFieldAndGetHTML(groupField);
5389 });
5390 //Записываем её в наш массив свойств
5391 propsIndex = fieldsPropsArray.length;
5392 fieldsPropsArray.push(groupHTML);
5393 //Возвращаем обёртку группы со всеми нужными параметрами. Внутрянку будем добавлять/удалять по мере необходимости
5394 return "<div class="groupContainer fieldName_${field.name} " +
5395 field.name +
5396 (field.flags && field.flags.includes('excluded') ? ' excluded' : '') +
5397 (field.flags && field.flags.includes('inline') ? ' inline' : '') +
5398 (field.flags && field.flags.includes('infinite') ? ' infinite' : '') +
5399 "" data-name="${field.name}" data-codepropindex="${propsIndex}"" +
5400 (field.limit ? " data-limit="${field.limit}"" : '') +
5401 (field.flags && field.flags.includes('excluded') ? ' data-excluded="excluded"' : '') +
5402 '></div>';
5403 case 'actionButtonGroup':
5404 case 'linkButtonGroup':
5405 //Подсказка
5406 help = '';
5407 if (field.titleHelp) {
5408 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>";
5409 }
5410
5411 groupHTML = field.title ? '<div class="stepTitle title">' + field.title + help + '</div>' : '';
5412
5413 if (!field.title) {
5414 field.title = '';
5415 }
5416
5417 if (!field.name) {
5418 field.name = '';
5419 }
5420
5421 if (!field.class) {
5422 field.class = field.name;
5423 }
5424
5425 Array.from(field.buttons).forEach(function (button) {
5426 button.service = button.type;
5427 button.type = 'linkButton';
5428 button.fieldType = field.type;
5429 button[field.type.toLowerCase()] = field.class;
5430 if (field.type === 'actionButtonGroup') {
5431 button.link = '';
5432 if (!button.iconClass) {
5433 button.iconClass = 'actionButtonNoIcon';
5434 }
5435 }
5436 if (!button.flags) {
5437 button.flags = [];
5438 }
5439 if (field.flags && field.flags.includes("block")) {
5440 button.flags.push("block");
5441 }
5442 if (!button.name) {
5443 button.name = field.name;
5444 }
5445 if (!button.class) {
5446 button.class = field.class;
5447 }
5448 if (!button.flags.includes('inline') && !button.flags.includes('block')) {
5449 button.flags.push('inline');
5450 }
5451 groupHTML += handleFieldAndGetHTML(button);
5452 });
5453 return '<div class="' + field.type + ' element ' + field.name + ' alien' +
5454 (field.flags && field.flags.includes('focused') ? ' focusMeOnStart' : '') +
5455 (field.flags && field.flags.includes('unfocusable') ? ' unfocusable ' : ' ') +
5456 field.class + '" data-name="' + field.name + '" data-title=\'' + field.title.replace(/'/g, '\"') + '\'>' + groupHTML + '</div>';
5457 case 'view':
5458 return Handlebars.compile("
5459 {{#v name="${field.name}" class="${field.class ? field.class : ''} ${field.flags && field.flags.includes('hidden') ? 'hidden' : ''}"}}
5460 ${field.content}
5461 {{/v}}
5462 ")(handlebarsContext);
5463 case 'null':
5464 return '';
5465 }
5466 ce('wrong_field_type', 'type:' + field.type + ', title:' + field.title + ', caption:' + field.caption);
5467};
5468
5469window.getFieldsHTML = (fields, context) => {
5470 let fieldsName = '';
5471 if (!context || !context[fields]) {
5472 if (this && this.getTask) {
5473 context = this.getTask().input_values;
5474 } else {
5475 context = window.handlebarsContext;
5476 }
5477 }
5478 if (fields && typeof fields === "string") {
5479 fieldsName = fields;
5480 fields = context[fieldsName];
5481 }
5482 if (!fields || !Array.isArray(fields)) {
5483 console.log(context);
5484 ce('Что-то неправильное передано хелперу drawFields: ', [fieldsName, fields ? fields : '[пустота]']);
5485 return;
5486 }
5487 let out = '';
5488 Array.from(fields).forEach(function (field) {
5489 out += handleFieldAndGetHTML.apply(this, [field, context]);
5490 });
5491 if (fieldsName) {
5492 out = "<div class="theOneFieldsContainer fieldsName_${fieldsName}" data-name="${fieldsName}">${out}</div>";
5493 }
5494 return out;
5495};
5496
5497
5498
5499
5500
5501
5502/* Всякие служебные переменные и JSONки */
5503
5504window.specObjectNameOfTheField = [];
5505window.fieldsPropsArray = [];
5506
5507//Настройки шаблона, работающие через CSS (если какая-то из них false, то в класс сьюта добавится "no_[имя настройки]", например "no_taskList")
5508window.cssNoClassSettings = ['taskSwitcher', 'taskCounter', 'taskList', 'taskMinimizer', 'notForDirect'];
5509window.cssClassSettings = ['fullScreenMode', 'verticalSuite'];
5510
5511//СВГшные иконки, доступные к использованию с классом ffIcon (в data-icon)
5512window.svgIcons = {
5513 "keyboard_arrow_up": {
5514 "attributes": 'viewBox="0 0 320 512"',
5515 "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"
5516 },
5517 "keyboard_tab": {
5518 "attributes": 'viewBox="0 0 448 512"',
5519 "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"
5520 },
5521 "keyboard_backspace": {
5522 "attributes": 'viewBox="0 0 448 512"',
5523 "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"
5524 },
5525 "invert_colors": {
5526 "attributes": 'viewBox="0 0 512 512"',
5527 "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"
5528 },
5529 "open_in_new": {
5530 "attributes": 'viewBox="0 0 576 512"',
5531 "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"
5532 },
5533 "done": {
5534 "attributes": 'viewBox="0 0 512 512"',
5535 "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"
5536 },
5537 "angle_down": {
5538 "attributes": 'viewBox="0 0 320 512"',
5539 "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"
5540 },
5541 "times": {
5542 "attributes": 'viewBox="0 0 352 512"',
5543 "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"
5544 },
5545 "eye": {
5546 "attributes": 'viewBox="0 0 576 512"',
5547 "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"
5548 },
5549 "comment_smile": {
5550 "attributes": 'viewBox="0 0 512 512"',
5551 "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"
5552 },
5553 "help": {
5554 "attributes": 'viewBox="0 0 448 512"',
5555 "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",
5556 "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"
5557 },
5558 "helpRound": {
5559 "attributes": 'viewBox="0 0 512 512"',
5560 "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"
5561 }
5562};
5563
5564//Возможные выводимые в консоль ошибки в шаблоне, для упрощения отладки и устранения проблем со входным JSON
5565window.templateErrorsJSON = {
5566 "output_placeholder_not_found":
5567 'Судя по всему, вы попытались использовать операторы в одном из полей JSONки - найдено сочетание "|||" Но вот имя оператора мне не понятно. Если вы ничего такого делать не собирались - пожалуйста, не используйте "|||" в JSONке.',
5568 "output_placeholder_input_insufficient_parameters":
5569 'Недостаточно параметров у оператора в JSON. У оператора INPUT три параметра - сам оператора, имя входного поля и текст, в который вместо плейсхолдера "▼" (Alt+31) будет подставлено значение этого поля.',
5570 "wrong_field_type":
5571 'Неверный тип у поля, вместо этого выведено "undefined"',
5572 "wrong_linkButtonGroup_button_type":
5573 'Неверный тип у кнопки в linkButtonGroup, она не выведена.',
5574 "no_field_name":
5575 'У поля отсутствует свойство name!',
5576 "no_button_name":
5577 'У кнопки отсутствует свойство name!',
5578 "valuesToWrite_is_not_an_array":
5579 'Свойство valuesToWrite не является массивом объектов!',
5580 "valuesToWrite_has_no_selector":
5581 'У одного из объектов массива valuesToWrite отсутствует свойство "selector". Укажите его, чтобы было понятно, в свойство property элемента по какому селектору вставлять значение value.',
5582 "valuesToWrite_has_no_property":
5583 'У одного из объектов массива valuesToWrite отсутствует свойство "property". Укажите его, чтобы было понятно, какое свойство заменять у указанного элемента с селектором selector (например, "value", если это input или textarea или "innerHTML" если это div или span).',
5584 "valuesToWrite_has_no_value":
5585 'У одного из объектов массива valuesToWrite отсутствует свойство "value". Укажите его, чтобы было понятно, какое именно значение вставлять в свойство property элемента с селектором selector.',
5586 "valuesToWrite_target_not_found":
5587 'Только что нажатая кнопка должна была прописать значение в свойство элемента по определённому селектору (свойство valuesToWrite во входном JSON), но нужный элемент по указанному селектору найти так и не удалось... ',
5588 "specmapper_conversion_type_not_found":
5589 'Указанного типа конверсии в правиле specMapper"а не существует.',
5590 "specmapper_conversion_no_function":
5591 'В specMapper"е указан тип конверсии - функция, но не указана сама функция.'
5592};
5593
5594//Возможные типы linkButton и их параметры
5595window.linkButtonTypes = {
5596 "yandexSearch": {
5597 iconClass: "yandexButtIcon",
5598 link: "https://yandex.ru/yandsearch?text=",
5599 caption: "Яндекс.Поиск"
5600 },
5601 "yandexPics": {
5602 iconClass: "yandexButtIcon",
5603 link: "https://yandex.ru/images/search?text=",
5604 caption: "Яндекс.Картинки"
5605 },
5606 "yandexSearchByPic": {
5607 iconClass: "yandexButtIcon",
5608 link: "https://yandex.by/images/search?url=",
5609 "linkAfter": "&rpt=imageview",
5610 caption: "Поиск по картинке"
5611 },
5612 "yandexNews": {
5613 iconClass: "yandexButtIcon",
5614 link: "https://news.yandex.ru/yandsearch?rpt=nnews2&grhow=clutop&text=",
5615 caption: "Яндекс.Новости"
5616 },
5617 "yandexVideo": {
5618 iconClass: "yandexButtIcon",
5619 link: "https://yandex.ru/video/search?text=",
5620 caption: "Яндекс.Видео"
5621 },
5622 "yandexTranslate": {
5623 iconClass: "yandexButtIcon",
5624 link: "https://translate.yandex.ru/?text=",
5625 caption: "Яндекс.Переводчик"
5626 },
5627 "googleSearch": {
5628 iconClass: "googleButtIcon",
5629 link: "https://www.google.ru/search?q=",
5630 caption: "Google.Поиск"
5631 },
5632 "googlePics": {
5633 iconClass: "googleButtIcon",
5634 link: "https://www.google.ru/search?&hl=ru&site=imghp&tbm=isch&source=hp&biw=1467&bih=682&q=",
5635 caption: "Google.Картинки"
5636 },
5637 "googleSearchByPic": {
5638 iconClass: "googleButtIcon",
5639 link: "https://www.google.by/searchbyimage?image_url=",
5640 caption: "Поиск по картинке"
5641 },
5642 "googleNews": {
5643 iconClass: "googleButtIcon",
5644 link: "https://www.google.ru/search?hl=ru&gl=ru&tbm=nws&authuser=0&q=",
5645 caption: "Google.Новости"
5646 },
5647 "googleVideo": {
5648 iconClass: "googleButtIcon",
5649 link: "https://www.google.ru/search?tbm=vid&hl=ru&source=hp&biw=&bih=&q=",
5650 caption: "Google.Видео"
5651 },
5652 "googleTranslate": {
5653 iconClass: "googleButtIcon",
5654 link: "https://translate.google.ru/#auto/ru/",
5655 caption: "Google.Переводчик"
5656 },
5657 //Украина
5658 "yandexSearchUA": {
5659 iconClass: "yandexButtIcon",
5660 link: "https://yandex.ua/search/?lr=143&msid=1464598804.64254.22885.27565&text=",
5661 caption: "Яндекс.Поиск (укр)"
5662 },
5663 "yandexVideoUA": {
5664 iconClass: "yandexButtIcon",
5665 link: "https://yandex.ua/video/search?text=",
5666 caption: "Яндекс.Видео (укр)"
5667 },
5668 "googleSearchUA": {
5669 iconClass: "googleButtIcon",
5670 link: "https://www.google.com.ua/#q=",
5671 caption: "Google.Поиск (укр)"
5672 },
5673 "googleVideoUA": {
5674 iconClass: "googleButtIcon",
5675 link: "https://www.google.com.ua/search?tbm=vid&hl=ru-UA&source=hp&biw=&bih=&q=",
5676 "linkAfter": "&tbm=vid",
5677 caption: "Google.Видео (укр)"
5678 }
5679};
5680
5681window.theOneCSS = "
5682body {
5683 overflow-x: hidden;
5684 font-size: 14px;
5685}
5686.displayNone {
5687 display: none !important;
5688}
5689.block {
5690 font-family: Helvetica,Arial,sans-serif;
5691 border: 0;
5692 border-radius: 4px;
5693 padding: 20px 10px 10px 10px;
5694 margin: 10px auto 35px;
5695 position: relative;
5696 background: #fff;
5697 max-width: 1200px;
5698 box-shadow: 5px 5px 18px #0001;
5699 text-align: left;
5700}
5701.task_focused .block {
5702 border: 1px solid #81c45600;
5703 box-shadow: 5px 5px 18px #0002;
5704}
5705.blockTitle {
5706 position: absolute;
5707 top: -15px;
5708 background: #e6e6e6;
5709 display: inline-block;
5710 padding: 8px 10px 6px 10px;
5711 font-size: 14px;
5712 line-height: 14px;
5713 border-radius: 4px;
5714 font-weight: 600;
5715 transition: all .2s ease-in 0s;
5716 margin-left: 8px;
5717}
5718.blockButtons {
5719 position: absolute;
5720 display: flex;
5721 top: -15px;
5722 right: 18px;
5723 transition: all .2s ease-in 0s;
5724}
5725.blockButtons .blockTitle {
5726 position: relative;
5727 display: block;
5728 width: fit-content;
5729 margin-left: 18px;
5730 padding: 0px;
5731 height: 28px;
5732 top: initial;
5733 left: initial;
5734 right: initial;
5735}
5736.task_focused .blockTitle {
5737 opacity: 1;
5738}
5739/*.deskTitle {
5740 font-weight: 600;
5741}*/
5742.buttDiv {
5743 margin-right: 10px;
5744}
5745.buttDiv:last-child {
5746 margin-right: 0;
5747}
5748.btn {
5749 margin-left: 9px;
5750 margin-bottom: 5px;
5751 display: inline-block;
5752 transition: max-height .3s ease-in, max-width .3s ease-in, opacity .2s ease-in;
5753 max-height: 50px;
5754 opacity: 1;
5755 user-select: none;
5756 cursor: pointer;
5757 position: relative;
5758 -moz-user-select: none;
5759}
5760.element .btn {
5761 margin-left: 0;
5762}
5763.btn a {
5764 font-size: 15px;
5765 color: #404040;
5766 display: inline-block;
5767 padding: 5px 12px 6px;
5768 background: #FFFBEA;
5769 margin: 4px 0 0;
5770 border-radius: 3px;
5771 border: 1px solid #d9d9d9;
5772 cursor: pointer;
5773 font-weight: 400;
5774 white-space: normal;
5775 position: relative;
5776 min-width: 16px;
5777 transition: all .3s ease-in-out 0s, box-shadow .1s ease-in 0s;
5778 text-align: center;
5779 max-width: calc(100vw - 80px);
5780 overflow: hidden;
5781}
5782.btn.hasHint a, .btn.hasHelp a {
5783 cursor: help;
5784}
5785.element:not(.asButtons) .btn a {
5786 text-align: left;
5787}
5788.btn.underPressure a {
5789 box-shadow: 0 0 1px 0 #0002 !important;
5790}
5791.btn.underPressure {
5792 left: 1px;
5793 top: 1px;
5794}
5795.btn.underPressure .popUpTitle {
5796 top: 39px;
5797 left: -1px;
5798}
5799.btn.noIcon a {
5800 padding-left: 13px;
5801}
5802.btn:hover a {
5803 box-shadow: 0 0 8px 1px #ffe478;
5804 z-index: 1;
5805}
5806.btn>i {
5807 position: absolute;
5808 left: 3px;
5809 font-size: 22px;
5810 top: 0px;
5811}
5812/*@media (max-width: 900px) {
5813 .btn a {
5814 font-size: 14px;
5815 padding: 2px 6px;
5816 font-weight: 400;
5817 }
5818 .darkSide .btn a {
5819 font-weight: 100;
5820 }
5821 .btn:not(.linkButton) i {
5822 font-size: 19px;
5823 top: 5px;
5824 }
5825}*/
5826table .btn a {
5827 padding: 3px 15px;
5828 margin-bottom: 8px;
5829}
5830.btn.active a {
5831 background: #ffe478;
5832}
5833.selected .btn, .filtered .btn.filterMe {
5834 max-height: 0;
5835 max-width: 0;
5836 margin: 0;
5837 opacity: 0;
5838}
5839.selected .buttons .btn.active {
5840 max-height: 50px;
5841 max-width: 400px;
5842 opacity: 1;
5843}
5844.selected .buttons {
5845 flex-wrap:nowrap;
5846}
5847.group {
5848 border-radius: 5px;
5849}
5850/*.stepNumber {
5851 z-index: -1;
5852 position: absolute;
5853 color: #ffe4789e;
5854 font-weight: 900;
5855 font-size: 55px;
5856 right: 2px;
5857 top: 0;
5858 opacity: .6;
5859 font-family: Helvetica, sans-serif;
5860 height: 141px;
5861 line-height: 94px;
5862}*/
5863.answerBlock {
5864 position: relative;
5865}
5866.stepQuestion {
5867 font-size: 14px;
5868 font-weight: 400;
5869 white-space: pre-wrap;
5870}
5871.stepTitle {
5872 font-weight: 600;
5873 margin-bottom: 5px;
5874}
5875.step {
5876 z-index: 9;
5877 position: relative;
5878 max-height: 0px;
5879 overflow: hidden;
5880 transition: all 0.5s cubic-bezier(0.6, 0, 0.4, 1);
5881}
5882.open {
5883 max-height: 2000px;
5884 overflow: visible;
5885}
5886/*.commentBlock .field_type_textarea {
5887 width: 100% !important;
5888 max-width: 500px;
5889}
5890.commentHint {
5891 font-size: 14px;
5892}
5893.field.field_type_checkbox {
5894 position: absolute;
5895}
5896.field.field_type_checkbox label {
5897 display: none;
5898}*/
5899.btn.btnDarkSide a {
5900 box-shadow: 5px 5px 18px #0002;
5901 margin: 0 20px 20px;
5902 padding: 4px;
5903 line-height: 12px;
5904 width: 26px;
5905 height: 26px;
5906 cursor: pointer;
5907}
5908.btn.btnDarkSide a i {
5909 top: 6px;
5910 left: 6px;
5911}
5912.btn.btnDarkSide {
5913 width: 100%;
5914 text-align: center;
5915 display: none;
5916 max-width: initial;
5917 margin: 0 auto;
5918 max-height: 100px;
5919}
5920.btn[name=result]>i {
5921 left: 0.45em;
5922}
5923.task-suite .task:first-child .btnDarkSide {
5924 display: block;
5925}
5926.darkButt {
5927 padding: 0 2px;
5928}
5929.blockButtons .blockTitle:not(.custom) .ffIcon {
5930 opacity: 0.6;
5931 cursor: pointer;
5932 transition: .3s ease-out;
5933 font-size: 28px;
5934}
5935.blockButtons .blockTitle:not(.custom):hover .ffIcon {
5936 opacity: 1;
5937}
5938.task:not(.task_focused) .blockTitle:not(.tabScroller) {
5939 display: none;
5940}
5941.btnDarkSide {
5942 display: none;
5943}
5944.task:first-child .btnDarkSide {
5945 display: block;
5946}
5947.task-suite:not(.verticalSuite) .task {
5948 max-height: 100vh;
5949 overflow-y: scroll;
5950 margin: 0;
5951 min-height: 100vh;
5952}
5953.task-suite {
5954 padding: 0;
5955 margin: 0;
5956 width: 100000px;
5957 min-height: 100vh;
5958 display: block;
5959 background: #f2f2f2;
5960 background-attachment: fixed !important;
5961 position: relative;
5962 left: -000vw;
5963 transition: left 0.3s ease-out;
5964}
5965.task {
5966 margin: 0 0 40px;
5967 display: inline-block;
5968 width: 100vw;
5969 background: none;
5970 border: 0 none !important;
5971 box-shadow: none !important;
5972 text-align: center;
5973 opacity: 0.7;
5974 transition: all .3s ease-in 0s;
5975}
5976.task.task_focused {
5977 opacity: 1;
5978}
5979/*@media (min-width: 1600px) {
5980 .task {
5981 width: calc(50% - 5px);
5982 }
5983}*/
5984.task:last-of-type {
5985 margin-bottom: 12px;
5986}
5987.task-suite.darkSide {
5988 /*background:
5989 radial-gradient(black 15%, transparent 16%) 0 0,
5990 radial-gradient(black 15%, transparent 16%) 8px 8px,
5991 radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 0 1px,
5992 radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 8px 9px;*/
5993 background-color: #253548;
5994 background-size:16px 16px;
5995}
5996.darkSide .block {
5997 /*background: rgba(16, 32, 85, 0.53);*/
5998 background:#23272B;
5999 color: #DCD8C7;
6000}
6001.darkSide .blockTitle {
6002 background-color: #E0DAC0;
6003 background-size: 20px 20px;
6004 color: #262626;
6005}
6006.darkSide .btn a {
6007 color: #262626;
6008 background: #E0DAC0;
6009 border: 1px solid #E0DAC0;
6010 font-weight: 400;
6011}
6012.darkSide .btn:hover a {
6013 border: 1px solid #585B5E;
6014 box-shadow: 0 0 0 0;
6015 background: #585B5E !important;
6016 color: #FFFBEA;
6017}
6018.darkSide .btn.active a {
6019 background: #4A4E52;
6020 color:#FFFBEA;
6021 border: 1px solid #585B5E;
6022}
6023.darkSide td {
6024 color: #e6e6e6;
6025}
6026.darkSide input, .darkSide textarea {
6027 background: #4A4E52 !important;
6028 border: 1px solid #585B5E !important;
6029 color: #FFFBEA !important;
6030}
6031.darkSide input::placeholder, .darkSide textarea::placeholder {
6032 color: #e6e6e6 !important;
6033}
6034.darkSide input::-webkit-input-placeholder, .darkSide textarea::-webkit-input-placeholder {
6035 color: #e6e6e6;
6036}
6037.darkSide input[disabled], .darkSide textarea[disabled] {
6038 color: #899094 !important;
6039}
6040.darkSide input.validationRejected, .darkSide textarea.validationRejected {
6041 box-shadow: 0px 0px 6px 0px #f33;
6042}
6043.darkSide input.validationPassed, .darkSide textarea.validationPassed {
6044 box-shadow: 0px 0px 6px 0px #3fbd4c;
6045}
6046.darkSide .icon_upload {
6047 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");
6048}
6049.hiddenField label {
6050 display: none;
6051}
6052.entityBlock, .classBlock {
6053 user-select: none;
6054}
6055i.material-icons {
6056 opacity: 0.9;
6057}
6058input {
6059 display: block;
6060 margin: 5px 0 0px;
6061 width: 100%;
6062 opacity: 1;
6063 border: 1px solid #d9d9d9;
6064 border-radius: 3px;
6065 padding: 5px;
6066 font-size: 14px;
6067}
6068textarea {
6069 display: block;
6070 width: 100%;
6071 min-height: 50px;
6072 max-height: 600px;
6073 margin-top: 5px;
6074 border: 1px solid #d9d9d9 !important;
6075 border-radius: 3px;
6076 padding: 5px;
6077 font-size: 14px;
6078}
6079textarea:focus {
6080 border: 1px solid #b3b3b3;
6081}
6082table tr td:last-child {
6083 font-size: 0.9em;
6084}
6085.clickableLinkTitle, .labelTitle {
6086 font-size: 14px;
6087 font-weight: 400;
6088}
6089.blockContent .label, .blockContent .link {
6090 margin-bottom: 10px;
6091 font-size: 14px;
6092 font-weight: 400;
6093}
6094.blockContent {
6095 width: 100%;
6096}
6097.stepTitle.title {
6098 margin-bottom: 10px;
6099}
6100/*.stepTitle.title:first-letter {
6101 color: #000;
6102 \tfont-size: 120%;
6103 \tfont-weight: 600;
6104}*/
6105.stepQuestion, .stepTitle {
6106 word-break: break-word;
6107}
6108.link {
6109 margin-bottom: 5px;
6110 word-wrap: break-word;
6111}
6112.link a {
6113 color: #A24F00;
6114 font-weight: 600;
6115 word-break: break-all;
6116}
6117.link i {
6118 font-size: 18px;
6119 position: relative;
6120 top: 3px;
6121 margin-right: 3px;
6122}
6123.darkSide .link a, .darkSide a {
6124 color: #ec7e00;
6125}
6126input.validationRejected, textarea.validationRejected {
6127 box-shadow: 0px 0px 6px 0px #f33;
6128}
6129input.validationPassed, textarea.validationPassed {
6130 box-shadow: 0px 0px 6px 0px #3fbd4c;
6131}
6132.groupContainer {
6133 max-height: 10000px;
6134 opacity: 1;
6135 transition: all .3s ease-in-out 0s;
6136 vertical-align: top;
6137}
6138.groupContainer.excluded {
6139 overflow: hidden;
6140}
6141.inline {
6142 display: inline-block;
6143}
6144.groupContainer.inline .group {
6145 border: 0 none;
6146 display: inline-block;
6147 padding: 0;
6148}
6149.groupContainer.inline.excluded {
6150 max-width: 0;
6151}
6152.groupContainer.infinite .groupContainer.infinite {
6153 margin-left: 20px;
6154}
6155.group.infiniteChild.infiniteSkipped {
6156 opacity: 0.6;
6157}
6158.buttDiv.inline {
6159 display: inline-block;
6160}
6161.excluded {
6162 max-height: 0 !important;
6163 opacity: 0;
6164}
6165.block.excluded, .block.main .block.excluded {
6166 margin: 0 !important;
6167 padding: 0 !important;
6168}
6169.switchButton>i {
6170 z-index: 2;
6171 font-size: 23px;
6172 top: calc(50% - 10px);
6173 left: 6px;
6174 color: #404040;
6175 opacity: 0;
6176 transition: all .3s ease-in-out 0s;
6177}
6178.switchButton.active>i {
6179 opacity: 0.9;
6180}
6181.darkSide .switchButton>i {
6182 color: #FFFBEA;
6183}
6184.switchButton.active a, .linkButton a {
6185 padding-left: 30px;
6186}
6187/*.stepTitle a {
6188 color: darkred;
6189 font-weight: 500;
6190}
6191.darkSide .stepTitle a {
6192 color: greenyellow;
6193}*/
6194.inputData > .stepTitle {
6195 font-weight: 600;
6196}
6197.title {
6198 margin: 1px 0 0px;
6199}
6200.element {
6201 padding: 9px;
6202 border-radius: 3px;
6203 margin-top: 2px;
6204 position: relative;
6205}
6206.element.focused {
6207 /*background: #57738033;*/
6208 background: rgba(0, 0, 0, 0.05);
6209}
6210.darkSide .element.focused {
6211 background: rgba(255, 255, 255, 0.07);
6212}
6213.elementTitle {
6214 font-weight: 600;
6215 font-size: 15px;
6216}
6217.hotkeyHint {
6218 position: absolute;
6219 right: 4px;
6220 top: 7px;
6221 font-size: 9px;
6222 opacity: 0;
6223 color: #888;
6224 z-index: 2;
6225}
6226.focused .hotkeyHint {
6227 opacity: 1;
6228 transition: all .9s ease-out 0s;
6229}
6230/*.butt {
6231 display: inline-block;
6232 margin-right: 6px;
6233 margin-left: 10px;
6234 margin-bottom: 5px;
6235 transition: max-height .3s ease-in, max-width .3s ease-in, opacity .2s ease-in;
6236 max-height: 50px;
6237 opacity: 1;
6238 cursor: pointer;
6239 position: relative;
6240 user-select: none;
6241}
6242.butt a {
6243 font-size: 15px;
6244 color: #404040;
6245 display: inline-block;
6246 padding: 8px 12px;
6247 background: #fc0;
6248 margin: 4px 0 0;
6249 border-radius: 3px;
6250 border: 1px solid #fc0;
6251 cursor: pointer;
6252 font-weight: 400;
6253 white-space: nowrap;
6254 position: relative;
6255 min-width: 16px;
6256 transition: all .3s ease-in-out 0s;
6257 text-align: center;
6258 max-width: calc(100vw - 80px);
6259 overflow: hidden;
6260 height: 32px;
6261 line-height: 14px;
6262}
6263.butt a:hover {
6264 background:#f2c200;
6265 border: 1px solid #f2c200;
6266 box-shadow: 0 0 10px 1px #ffe478;
6267}
6268.darkSide .butt a {
6269 border: 1px solid #E0DAC0;
6270 background: #E0DAC0;
6271 color: #262626;
6272}
6273.darkSide .butt a:hover {
6274 background:#e0e3e5;
6275 border: 1px solid #e0e3e5;
6276 box-shadow: 0 0 8px 1px #7296a8;
6277}*/
6278.task.noErrors .task__error {
6279 display: none;
6280}
6281.field_file__label {
6282 height: 15px;
6283}
6284.stepTitle.title {
6285 font-weight: 400;
6286 font-size: 14px;
6287 margin-bottom: 1px;
6288}
6289.linkButtonIconContainer {
6290 background: none;
6291 width: 24px;
6292 height: 24px;
6293 overflow: hidden;
6294 border-radius: 12px;
6295 position: absolute;
6296 top: 8px;
6297 left: 4px;
6298 opacity: 0.8;
6299 user-select: none;
6300 z-index: 2;
6301}
6302.darkSide .linkButtonIconContainer {
6303 background: none;
6304}
6305.linkButton i {
6306 display: inline-block;
6307 width: 17px;
6308 height: 17px;
6309 margin: 2px 3px;
6310 background-repeat: no-repeat;
6311}
6312
6313/* Новые чекбоксы и радиобаттоны */
6314.checkboxGroup:not(.asButtons) .stepTitle {
6315 margin-bottom: 4px;
6316}
6317.checkboxGroup:not(.asButtons) .btn>i {
6318 top: 0px;
6319 left: -1px;
6320}
6321.checkboxGroup:not(.asButtons) .btn a {
6322 border: 0 none;
6323 background: none;
6324 margin: 0;
6325 padding: 1px 15px 1px 0px;
6326}
6327.checkboxGroup:not(.asButtons) .btn a:before {
6328 content: "";
6329 display: inline-block;
6330 background: #fff;
6331 width: 15px;
6332 height: 15px;
6333 border-radius: 3px;
6334 margin-right: 8px;
6335 position: relative;
6336 top: 3px;
6337 border: 1px solid #80808080;
6338}
6339.checkboxGroup:not(.asButtons) .btn.active a:before {
6340 background: #ffe478;
6341}
6342.checkboxGroup:not(.asButtons) .btn:hover a {
6343 box-shadow: none;
6344}
6345.darkSide .checkboxGroup:not(.asButtons) .btn.active a, .darkSide .checkboxGroup:not(.asButtons) .btn a {
6346 background: none;
6347 border: none;
6348 color: #FFFBEA;
6349}
6350.darkSide .checkboxGroup:not(.asButtons) .btn>i {
6351 color: #000;
6352 width: 19px;
6353 left: -1px;
6354 top: 1px;
6355}
6356
6357.radioGroup:not(.asButtons) .stepTitle {
6358 margin-bottom: 4px;
6359}
6360.radioGroup:not(.asButtons) .btn>i {
6361 top: 10px;
6362 left: 6px;
6363 width: 7px;
6364 background: #000;
6365 height: 7px;
6366 border-radius: 5px;
6367}
6368.radioGroup:not(.asButtons) .btn>i svg {
6369 display: none;
6370}
6371.radioGroup:not(.asButtons) .btn a {
6372 border: 0 none;
6373 background: none;
6374 margin: 0;
6375 padding: 1px 15px 1px 0px;
6376}
6377.radioGroup:not(.asButtons) .btn a:before {
6378 content: "";
6379 display: inline-block;
6380 background: #fff;
6381 width: 17px;
6382 height: 17px;
6383 border-radius: 17px;
6384 margin-right: 8px;
6385 position: relative;
6386 top: 3px;
6387 border: 1px solid #80808080;
6388}
6389.radioGroup:not(.asButtons) .btn.active a:before {
6390 background: #ffe478;
6391}
6392.radioGroup:not(.asButtons) .btn:hover a {
6393 box-shadow: none;
6394}
6395.darkSide .radioGroup:not(.asButtons) .btn.active a, .darkSide .radioGroup:not(.asButtons) .btn a {
6396 background: none;
6397 border: none;
6398 color: #FFFBEA;
6399}
6400
6401/* Переопределения для вертикального сьюта */
6402.task-suite.verticalSuite {
6403 width: 100%;
6404 overflow-x: hidden;
6405}
6406
6407
6408/* Перелистывание вкладок */
6409.tabScroller {
6410 right: 18px;
6411 height: 23px !important;
6412 padding-top: 5px !important;
6413 z-index: 200;
6414 user-select: none;
6415}
6416.tabScroller i {
6417 line-height: 0;
6418 font-size: 32px;
6419 font-weight: 500;
6420 display: none;
6421 margin: -10px 0;
6422}
6423.leftScroll i:nth-of-type(2) {
6424 transform: rotate(180deg);
6425}
6426.rightScroll i:first-child {
6427 transform: rotate(180deg) !important;
6428}
6429.tabScroller i:nth-of-type(odd) {
6430 display: inline-block;
6431}
6432.task:first-of-type .tabScroller .leftScroll i:first-of-type, .task:last-of-type .tabScroller .rightScroll i:first-of-type{
6433 display: none;
6434}
6435.task:first-of-type .tabScroller .leftScroll i:last-of-type, .task:last-of-type .tabScroller .rightScroll i:last-of-type {
6436 display: inline-block;
6437}
6438.curTab, .tabsCount {
6439 margin: 0px 0px;
6440}
6441.leftScroll i:last-of-type, .rightScroll i:last-of-type {
6442 color: #808080;
6443}
6444.leftScroll, .rightScroll {
6445 cursor: pointer;
6446}
6447.tabNumbers {
6448 font-size: 16px;
6449 font-weight: 600;
6450 margin: 0 10px;
6451 top: 1px;
6452 position: relative;
6453 font-family: sans-serif;
6454 opacity: 0.8;
6455}
6456.verticalSuite .leftScroll svg, .verticalSuite .rightScroll svg {
6457 transform: rotate(90deg);
6458}
6459.verticalSuite .tabScroller {
6460 padding-left: 0;
6461 padding-right: 0;
6462}
6463.tabScroller .ffIcon {
6464 opacity: 0.6;
6465 transition: .3s ease-out;
6466}
6467.tabScroller:hover .ffIcon {
6468 opacity: 1;
6469}
6470
6471/* Сворачивалка блоков */
6472.block.minimizable .blockTitle {
6473 cursor: pointer;
6474}
6475.miniMaxi {
6476 user-select: none;
6477}
6478.miniMaxi i {
6479 font-size: 24px;
6480 transform: rotate(180deg);
6481 transition: all .2s ease-in 0s;
6482 padding: 0px;
6483 opacity: 0.8;
6484 margin: -7px -3px -7px 4px;
6485}
6486.minimized .miniMaxi i {
6487 transform: rotate(270deg);
6488}
6489.block:not(.minimizable)>div>div>.miniMaxi {
6490 display: none;
6491}
6492.blockContent {
6493 /* max-height: 1500px; */
6494 transition: max-height .5s ease-out 0s, overflow 0s ease 1s;
6495 overflow: visible;
6496}
6497.blockContent.minimized {
6498 max-height: 0px;
6499 overflow: hidden;
6500}
6501.blockContent>.helpContainer {
6502 position: absolute;
6503 left: auto;
6504 right: 3px;
6505 top: 2px;
6506}
6507.block:not(:hover)>.blockContent>.helpContainer:not(.triggered) {
6508 opacity: 0;
6509}
6510.minimized .searchField {
6511 opacity: 0;
6512}
6513.popUpTitle {
6514 padding: 4px 8px;
6515 position: absolute;
6516 left: 0px;
6517 top: 15px;
6518 color: black;
6519 white-space: pre-line;
6520 width: 400px;
6521 font-size: 14px;
6522 border: 1px solid #A6A6A6;
6523 z-index: 3000;
6524 border-radius: 5px;
6525 background: #fffe;
6526 opacity: 0;
6527 transition: opacity .3s ease-in-out 0s, visibility 0s ease 1.2s;
6528 visibility: hidden;
6529 box-shadow: 0 0 10px #000A;
6530}
6531div:not(.helpContainer)>.popUpTitle:hover {
6532 display: none;
6533}
6534/*.searchField .popUpTitle {
6535 line-height: initial;
6536}*/
6537.btn .popUpTitle {
6538 top: 40px;
6539}
6540.elementBody .popUpTitle {
6541 top: 40px;
6542}
6543.upperPopups .popUpTitle {
6544 top: initial;
6545 bottom: 35px;
6546}
6547.darkSide .popUpTitle {
6548 background: #222e;
6549 border-color: #666;
6550 color: snow;
6551}
6552.helpContainer {
6553 vertical-align: top;
6554 display: inline-block;
6555 position: relative;
6556 left: 2px;
6557 font-size: 15px;
6558 cursor: default;
6559 opacity: 1;
6560 top: 5px;
6561 transition: all .3s ease-out 0s;
6562}
6563.helpContainer .ffIcon {
6564 cursor: pointer;
6565 padding: 0;
6566 color: #A6A6A6;
6567 margin: 0;
6568 transition: color .3s ease-in;
6569 top: -1px;
6570 position: relative;
6571}
6572.blockContent>.helpContainer .ffIcon {
6573 width: 1.2em;
6574 height: 1.2em;
6575}
6576.helpContainer.triggered .popUpTitle, .helpContainer.openedByKey .popUpTitle, .openedByKey .popUpTitle {
6577 opacity: 1;
6578 visibility: visible;
6579 transition: opacity .3s ease-in-out 0s, visibility 0s ease 0s;
6580}
6581.helpContainer .popUpTitle {
6582 transition: opacity .3s ease-in-out 0s, visibility 0s ease .3s;
6583 display: flex;
6584 padding: 1px;
6585}
6586/*.element:not(.focused):not(:hover) .helpContainer {
6587 transition: none;
6588}*/
6589.helpContainer .ffIcon:hover, .helpContainer.openedByKey .ffIcon {
6590 color: #737373;
6591}
6592.darkSide .helpContainer .ffIcon {
6593 color: #808080;
6594}
6595.darkSide .helpContainer .ffIcon:hover, .darkSide .helpContainer.openedByKey .ffIcon {
6596 color: #BFBFBF;
6597}
6598.element:not(.focused):not(:hover) .helpContainer {
6599 opacity: 0;
6600}
6601.elementTitle>.stepQuestion>.helpContainer, .element.fileField>.stepTitle>.helpContainer {
6602 display: inline-block;
6603 top: 1px;
6604}
6605.element>.stepTitle>.helpContainer {
6606 top: 2px;
6607}
6608.element:not(:hover) .elementTitle>.stepQuestion>.helpContainer:not(.triggered) {
6609 opacity: 0;
6610}
6611.buttPath {
6612 font-weight: 600;
6613}
6614.elementBody {
6615 position: relative;
6616}
6617.cursorHelp {
6618 cursor: help;
6619}
6620.popUpTitleInner {
6621 overflow-y: auto;
6622 padding: 3px 7px;
6623 word-break: break-word;
6624}
6625/*.suggest:hover .popUpTitle, */.btn:hover .popUpTitle, .elementBody:hover .popUpTitle, .selectHead:hover .popUpTitle {
6626 opacity: 1;
6627 transition: opacity .3s ease-in-out .9s, visibility 0s ease .9s;
6628 visibility: visible;
6629 }
6630/*.suggest.final {
6631 order: 0;
6632 background: #cdf9c4;
6633}
6634.suggest.final:hover {
6635 background: #88ff71;
6636}
6637.suggest.notFinal {
6638 order: 1;
6639 background: #ffefb2;
6640}
6641.suggest.notFinal:hover {
6642 background: #ffdb4d;
6643}
6644.darkSide .suggest.final {
6645 background: #386b00;
6646}
6647.darkSide .suggest.notFinal {
6648 background: #715a00;
6649}
6650.darkSide .suggest.notFinal:hover {
6651 background: #a98700;
6652}
6653.darkSide .suggest.final:hover {
6654 background: #5baf00;
6655}*/
6656.img__container__imitation {
6657 background-position: 0%;
6658}
6659
6660/* Вывод ошибок */
6661.errorMsgBlock {
6662 position: absolute;
6663 color: white;
6664 bottom: 100%;
6665 background: #fa7067;
6666 padding: 2px 2px;
6667 font-size: 14px;
6668 border-width: 7px;
6669 display: none;
6670 white-space: pre-line;
6671 z-index: 1000;
6672}
6673.withSomeError .errorMsgBlock {
6674 display: flex;
6675}
6676.errorMsgBlock:after {
6677 border-width: 9px;
6678 content: " ";
6679 bottom: -18px;
6680 z-index: 100;
6681 border-style: solid;
6682 pointer-events: none;
6683 left: 10px;
6684 height: 0;
6685 width: 0;
6686 background: 0 none;
6687 position: absolute;
6688 border-color: #fa7067 #0000 #0000;
6689}
6690.errorMsgBlockInner {
6691 overflow-y: auto;
6692 max-height: 100%;
6693 padding: 2px 5px 2px 5px;
6694}
6695.darkSide .errorMsgBlock {
6696 background: #fb5c56;
6697}
6698.darkSide .errorMsgBlock:before {
6699 border-color: #fb5c56 #0000 #0000;
6700}
6701.element.withSomeError {
6702 background-color: #f9baaf33;
6703}
6704.element.focused.withSomeError {
6705 background-color: #b7585833;
6706}
6707.darkSide .element.withSomeError {
6708 background-color: #ff000014;
6709}
6710.darkSide .element.focused.withSomeError {
6711 background-color: #ff19192e;
6712}
6713
6714/* Список тасков в сьюте */
6715.taskName {
6716 display: none;
6717}
6718.tabList {
6719 width: max-content;
6720 position: absolute;
6721 right: 0px;
6722 transition: max-height .2s cubic-bezier(0, 0.3, 0.24, 0.59) 0s;
6723 max-width: 50vw;
6724 cursor: default;
6725 overflow: hidden;
6726}
6727.tabScroller:hover .tabList {
6728 transition: max-height .2s cubic-bezier(0, 0.3, 0.24, 0.59) 0.7s;
6729}
6730.tabList>div {
6731 z-index: 1000;
6732 border: 1px solid #81c45600;
6733 border-radius: 5px;
6734 padding: 5px 0;
6735 position: relative;
6736 margin-top: 10px;
6737 top: 0;
6738 background: white;
6739 box-shadow: 5px 5px 18px #0002;
6740}
6741.tabListItemNumber, .tabListItemName {
6742 display: inline-block;
6743 margin: 1px 7px 1px 14px;
6744}
6745.tabListItemNumber {
6746 font-size: 11px;
6747 position: absolute;
6748 left: -10px;
6749 top: 4px;
6750}
6751.tabListItem {
6752 opacity: 0.7;
6753 padding: 3px 0px 3px 4px;
6754 font-weight: 400;
6755 position: relative;
6756 margin: 2px 5px 2px 4px;
6757 cursor: pointer;
6758}
6759.tabListItem.selected {
6760 color: #404040;
6761 opacity: 0.8;
6762 font-weight: 600;
6763 cursor: default;
6764 background: #ffe478;
6765 border-radius: 3px;
6766}
6767.tabListItem:not(.selected):hover {
6768 opacity: 1;
6769}
6770.darkSide .tabList>div {
6771 border-color: #7296a8;
6772 background-color: #E0DAC0;
6773 color: #3a454b;
6774}
6775.darkSide .tabListItem.selected {
6776 color: #253548;
6777 opacity: 0.93;
6778}
6779.task:not(.task_focused) .tabList>div {
6780 border-color: #cccccc;
6781}
6782.task:not(.task_focused) .tabListItem.selected {
6783 background-color: #d9d9d9;
6784}
6785/* Директ */
6786.no_notForDirect .bannerContainer {
6787 border: 1px solid #bbb;
6788 padding: 10px;
6789 width: fit-content;
6790 border-radius: 2px;
6791 display: inline-block;
6792 text-align: left;
6793}
6794.no_notForDirect .bannerTitle {
6795 font-size: 16px;
6796 line-height: 20px;
6797 position: relative;
6798 font-weight: bold;
6799 margin-bottom: 5px;
6800 display: flex;
6801}
6802.no_notForDirect.darkSide .bannerTitle a {
6803 color: aqua;
6804}
6805.no_notForDirect .bannerDomain {
6806 font-size: 12px;
6807 line-height: 16px;
6808 color: rgb(0, 102, 0);
6809 font-weight: bold;
6810 margin-top: 8px;
6811}
6812.no_notForDirect.darkSide .bannerDomain {
6813 color: #36e400;
6814}
6815.no_notForDirect .fieldName_verdict .buttDiv.fieldName_Yes {
6816 width: calc(25% - 7px);
6817}
6818.no_notForDirect .fieldName_checkboxes {
6819 margin-left: 25%;
6820}
6821.no_notForDirect .bannerInfoLine>div {
6822 display: inline-block;
6823 margin-right: 5px;
6824}
6825.no_notForDirect .bannerInfoLine {
6826 margin-bottom: 3px;
6827}
6828.no_notForDirect .bilValue {
6829 font-weight: 600;
6830 margin: 0 !important;
6831}
6832.no_notForDirect .topFlexBox {
6833 display: flex;
6834 flex-wrap: wrap;
6835}
6836.no_notForDirect .bannerElement {
6837 flex-grow: 10;
6838 text-align: center;
6839 padding-top: 0;
6840 margin-right: 20px;
6841}
6842.no_notForDirect .bannerInfoElement {
6843 background: #81c456;
6844 padding: 7px 15px;
6845 border-radius: 5px;
6846 border: 1px solid #81c456;
6847 flex-shrink: 0;
6848 margin-left: 9px;
6849 margin-bottom: 9px;
6850}
6851.no_notForDirect .bannerTitle a {
6852 color: blue;
6853 transition: all .1s ease-in;
6854 opacity: 1;
6855}
6856.no_notForDirect .bannerTitle .ffIcon {
6857 display: none;
6858}
6859.no_notForDirect .bannerTitle.visited .ffIcon {
6860 display: block;
6861}
6862.no_notForDirect .bannerTitle a:hover {
6863 opacity: 0.75;
6864}
6865.no_notForDirect .bannerTitle.visited a {
6866 color: #7000ff;
6867}
6868.no_notForDirect.darkSide .bannerTitle.visited a {
6869 color: #b843ff;
6870}
6871.no_notForDirect .bannerTitle .ffIcon {
6872 font-size: 25px;
6873 position: relative;
6874 top: -3px;
6875 left: -2px;
6876 color: #bbb;
6877 margin: 0 -1px;
6878}
6879.no_notForDirect .popUpTitle {
6880 width: 555px;
6881}
6882@media (max-width: 768px) {
6883 .no_notForDirect .block {
6884 max-width: initial;
6885 }
6886 .helpContainer {
6887 display: none;
6888 }
6889}
6890.misprinted {
6891 color: red;
6892}
6893
6894/* SVGшки */
6895.yandexButtIcon i {
6896 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>');
6897 margin-left: 6px;
6898}
6899.googleButtIcon i {
6900 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>');
6901 left: -1px;
6902}
6903.undoButtIcon i {
6904 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>');
6905 margin-left: 1px;
6906}
6907.redoButtIcon i {
6908 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>');
6909 margin-left: 0px;
6910}
6911.syncButtIcon i {
6912 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>');
6913 margin-left: 1px;
6914}
6915.adjustButtIcon i {
6916 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>');
6917 margin-left: 1px;
6918 margin-top: 3px;
6919 width: 21px;
6920 height: 21px;
6921}
6922.adjustSolidButtIcon i {
6923 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>');
6924 margin-left: 1px;
6925 width: 18px;
6926 height: 18px;
6927}
6928.swatchBookIcon i {
6929 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>');
6930 margin-left: 1px;
6931 width: 18px;
6932 height: 18px;
6933}
6934.sunButtIcon i {
6935 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>');
6936 margin-left: 1px;
6937 width: 18px;
6938 height: 18px;
6939}
6940.paletteButtIcon i {
6941 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>');
6942 margin-left: 1px;
6943 width: 18px;
6944 height: 18px;
6945}
6946.expandButtIcon i {
6947 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>');
6948}
6949.washerButtIcon i {
6950 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>');
6951 margin-left: 1px;
6952}
6953.eraserButtIcon i {
6954 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>');
6955 margin-left: 1px;
6956}
6957.defaultButtIcon i {
6958 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>');
6959 margin-top: 4px;
6960}
6961.uploadButtIcon i {
6962 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>');
6963 margin-left: 1px;
6964}
6965.thrashButtIcon i {
6966 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>');
6967 margin-left: 1px;
6968}
6969.editButtIcon i {
6970 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>');
6971 margin-left: 1px;
6972}
6973.kinopoiskButtIcon i { /* by @stillnoise */
6974 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>');
6975}
6976.linkButtonIconContainer i {
6977 top: 0px;
6978 left: 2px;
6979}
6980
6981/* SVG-шные иконки */
6982.ffIcon {
6983 display: inline-block;
6984 width: 1em;
6985 height: 1em;
6986 padding: 3px;
6987 color: #111;
6988 box-sizing: border-box;
6989}
6990.ffIcon svg {
6991 display: block;
6992 width: 100%;
6993 height: 100%;
6994}
6995.darkSide .link .ffIcon {
6996 color: #e0dac0;
6997}
6998
6999/* Настройки шаблона */
7000.no_taskList .tabList {
7001 display: none;
7002}
7003.no_taskCounter .tabNumbers {
7004 width: 0 !important;
7005 overflow: hidden;
7006 display: inline-block;
7007 margin: 0 5px;
7008}
7009.no_taskSwitcher .tabScroller {
7010 display: none;
7011}
7012.no_taskMinimizer .miniMaxi {
7013 display: none;
7014}
7015.no_taskMinimizer .block.minimizable .blockTitle {
7016 cursor: default;
7017}
7018
7019/* Просмотрщик картинок (начиная с 1.20) */
7020.imageField {
7021 user-select: none;
7022}
7023.imageField .imageBlock {
7024 overflow: hidden;
7025 flex-grow: 10;
7026 position: relative;
7027}
7028.imageField .imageBlock img {
7029 position: relative;
7030}
7031.imageField .imageBlock img {
7032 position: relative;
7033 cursor: grab;
7034 transition: all .1s ease-out;
7035 transform: none;
7036}
7037.imageConsole {
7038 position: absolute;
7039 bottom: 0;
7040 right: 0;
7041 color: white;
7042 text-shadow: 0 0 10px black, 0 0 5px black;
7043 opacity: 0;
7044 transition: all .3s ease-out;
7045 margin: 0 2px 1px 0;
7046}
7047.imageField canvas {
7048 width: 100%;
7049 height: 400px;
7050 image-rendering: optimizeQuality;
7051 image-rendering: -moz-crisp-edges;
7052 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%);
7053 background-size: 20px 20px;
7054 background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
7055 cursor: grab;
7056}
7057.dragging.imageField canvas {
7058 cursor: grabbing;
7059}
7060.imageField.expanded {
7061 position: fixed;
7062 z-index: 3000;
7063 background: white;
7064 width: calc(100vw - 50px) !important;
7065 height: calc(100vh - 50px);
7066 left: 10px;
7067 top: 10px;
7068 padding: 15px;
7069 box-shadow: 0 0 35px 0px #000;
7070 display: flex;
7071 flex-direction: column;
7072 margin: 0 !important;
7073}
7074.imageField.expanded canvas {
7075 height: 100% !important;
7076 width: 100% !important;
7077}
7078.imageField .stepTitle {
7079 text-align: center;
7080 margin-bottom: 8px;
7081}
7082.imageField.expanded .stepTitle {
7083 display: none;
7084}
7085.imageFooter {
7086 display: flex;
7087 justify-content: space-between;
7088}
7089.imageFooter .btn a {
7090 width: 0px;
7091 overflow: hidden;
7092 color: #0000;
7093 padding-right: 0;
7094 padding-left: 12px;
7095 z-index: 10;
7096 background: #0000;
7097 margin-top: 0;
7098 white-space: nowrap;
7099 height: 17px;
7100}
7101.imageFooter .linkButtonIconContainer {
7102 text-align: center;
7103 top: 4px;
7104 left: 4px;
7105 opacity: 0.9;
7106}
7107.imageSource {
7108 display: none;
7109}
7110.imageOriginalLink {
7111 display: none;
7112}
7113.darkSide .imageFooter .btn:hover a {
7114 background: #0000 !important;
7115 color: #0000;
7116}
7117.imageFooter>div>.element {
7118 padding: 0;
7119}
7120.imageFooter>div>.element .btn {
7121 margin-bottom: 0;
7122 margin-top: 3px;
7123}
7124.imageFooter>div>.element .buttDiv:last-child {
7125 margin-right: 0;
7126}
7127.imageFooter .hotkeyHint {
7128 right: 2px;
7129 font-size: 8px;
7130 top: 1px;
7131}
7132.darkSide .imageField.expanded.focused {
7133 background: rgb(56, 68, 74);
7134 border: 1px solid #7296a8;
7135}
7136
7137.darkSide .imageField .btn a {
7138 border: 0 none;
7139}
7140.imageFooter>div>.element .btn {
7141 border-radius: 3px;
7142 margin-top: 6px;
7143 width: 30px;
7144 height: 29px;
7145 transition: all .2s ease-out;
7146 background: #FFFBEA;
7147}
7148.darkSide .imageFooter>div>.element .btn {
7149 background: #E0DAC0;
7150}
7151.darkSide .imageFooter>div>.element .btn.selected {
7152 background: #4A4E52;
7153}
7154.imageFooter>div>.element .btn.selected {
7155 background: #ffe478;
7156}
7157.imageRightButts .popUpTitle {
7158 left: initial;
7159 right: 0;
7160}
7161.imageField.noCanvas .imageConsole {
7162 display: none;
7163}
7164.imageField.noCanvas canvas {
7165 position: absolute;
7166 left: 0;
7167}
7168.imageField.noCanvas .justAnImage {
7169 object-fit: contain;
7170 width: 100%;
7171 height: 100%;
7172}
7173.imageField.noCanvas .imageBlock {
7174 max-height: 400px;
7175 text-align: center;
7176}
7177.imageField.noCanvas.expanded .imageBlock {
7178 max-height: 100%;
7179}
7180/* Select - элемент с версии 1.22 */
7181.select.element {
7182 position: relative;
7183 min-width: 200px;
7184 width: 300px;
7185 display: block;
7186}
7187.select.element .stepTitle {
7188 margin-bottom: 5px;
7189}
7190.selectContainer {
7191 max-height: 0;
7192 overflow: hidden;
7193 transition: all .3s ease-out;
7194 position: absolute;
7195 width: calc(100% - 20px);
7196 background: #f2f2f2;
7197 z-index: 1000;
7198 padding: 0 9px 0;
7199 border-radius: 3px;
7200 margin-left: -9px;
7201}
7202.select.opened .selectContainer {
7203 max-height: 800px;
7204 overflow-y: auto;
7205 padding: 0 10px 9px 9px;
7206}
7207.selectContainer .buttDiv {
7208 display: block;
7209 width: 100%;
7210 margin: 0;
7211}
7212.selectContainer .btn {
7213 width: 100%;
7214 margin: 0;
7215 max-height: initial;
7216}
7217.selectContainer .btn a {
7218 padding: 2px 24px 3px 14px;
7219 margin-top: -1px;
7220 display: block;
7221 width: min-content;
7222 min-width: calc(100% - 38px);
7223 white-space: pre-line;
7224 max-height: initial;
7225 text-align: left;
7226}
7227.selectContainer .btn i {
7228 top: calc(50% - 13px);
7229 left: initial;
7230 right: -1px;
7231}
7232.selectContainer .btn .hotkeyHint {
7233 right: initial;
7234 left: 3px;
7235 top: 1px;
7236}
7237.selectHead {
7238 border: 1px solid #d9d9d9;
7239 border-radius: 3px;
7240 height: 26px;
7241 display: flex;
7242 background: #FFFBEA;
7243}
7244.selectDisplay {
7245 flex-grow: 10;
7246 overflow: hidden;
7247 cursor: pointer;
7248}
7249.selectButtons i {
7250 width: 26px;
7251 height: 26px;
7252}
7253.selectButtons .empty {
7254 display: none;
7255}
7256.selectButtons .arrow svg {
7257 height: 30px;
7258 top: -4px;
7259 position: relative;
7260}
7261.selectButtons i {
7262 width: 26px;
7263 height: 26px;
7264 opacity: 0.5;
7265 cursor: pointer;
7266 transition: all .2s ease-out;
7267}
7268.selectButtons i:hover {
7269 opacity: 1;
7270}
7271.selectText {
7272 width: 2000px;
7273 padding: 4px 8px;
7274}
7275.darkSide .selectText {
7276 color: #23272b;
7277}
7278.darkSide .select .selectHead, .darkSide .blockTitle .select .selectHead {
7279 background: #E0DAC0;
7280 border: 1px solid #E0DAC0;
7281}
7282.darkSide .select .selectContainer {
7283 padding: 0 4px 0 2px;
7284 left: 16px;
7285}
7286.darkSide .select.opened .selectContainer {
7287 padding: 3px 4px 2px 2px;
7288}
7289.select:not(.opened) .ffIcon.arrow {
7290 transform: rotate(90deg);
7291}
7292.select.opened .ffIcon.arrow {
7293 transform: rotate(0deg);
7294}
7295.blockTitle .select .selectHead {
7296 background-color: #e6e6e6;
7297}
7298.blockTitle .select {
7299 padding: 0;
7300 margin: 0;
7301}
7302.blockTitle .select .selectContainer {
7303 padding: 0px 4px 0px 2px;
7304 left: 8px;
7305 top: 29px;
7306 width: calc(100% - 4px);
7307}
7308.blockTitle .select.opened .selectContainer {
7309 padding: 2px 4px 2px 2px;
7310}
7311.blockTitle .select .selectText {
7312 padding-top: 6px;
7313 opacity: 0.8;
7314}
7315.blockTitle .select.focused .selectHead {
7316 background-color: #d9d9d9;
7317}
7318.darkSide .blockTitle .select.focused .selectHead {
7319 background-color: #d9d9d9;
7320}
7321.selectHead .popUpTitle {
7322 top: 60px;
7323 left: 9px;
7324}
7325.selectContainer .btn .popUpTitle {
7326 max-width: calc(100% - 20px);
7327 top: -2000px;
7328 visibility: hidden;
7329}
7330.selectContainer .btn:hover .popUpTitle {
7331 top: 20px;
7332 display: block;
7333 visibility: visible;
7334}
7335/* Загрузчик файлов */
7336.fileField .fcFileLine {
7337 display: flex;
7338}
7339.fileField .fcFileLine div {
7340 font-weight: 600;
7341 padding: 0;
7342}
7343.fileField .fcFileName {
7344 display: flex;
7345 align-items: center;
7346 margin-right: 10px !important;
7347 margin-left: 10px;
7348}
7349.fileField .fcFileLine.uploading .fcFileName {
7350 opacity: .9;
7351 animation: blink 1.2s linear infinite;
7352}
7353.fileField .fcontainer.somethingIsUploading .btn.uploadButtIcon, .fileField .fcontainer.somethingIsUploading .btn.uploadButtIcon a {
7354 opacity: 0.7;
7355 left: 0px;
7356 top: 0px;
7357 cursor: wait;
7358}
7359.fileField .btn a {
7360 padding-top: 2px;
7361 padding-bottom: 3px;
7362 color: black;
7363}
7364.fileField .fUploader .element {
7365 margin: 0 0 0 15px;
7366 padding: 0;
7367}
7368.fileField .linkButtonIconContainer {
7369 top: 6px;
7370 width: 20px;
7371}
7372.fileField>.stepTitle {
7373 display: flex;
7374 justify-content: flex-start;
7375 flex-wrap: wrap;
7376 align-items: center;
7377}
7378.fileField>.stepTitle .btn {
7379 margin: 0 0 4px;
7380}
7381.fileField .errorMsgBlock {
7382 bottom: 45px;
7383}
7384/* Модальные окна */
7385.window {
7386 position: fixed;
7387 left: 0;
7388 top: 0;
7389 width: 100vw;
7390 height: 100vh;
7391 max-width: initial;
7392 max-height: initial;
7393 z-index: 1002;
7394 margin: 0;
7395 padding: 0;
7396 background: #0008;
7397 display: none;
7398 justify-content: center;
7399 align-items: center;
7400}
7401.window.opened {
7402 display: flex;
7403}
7404.windowButtons>div {
7405 text-align: right;
7406 padding-right: 0;
7407 padding-bottom: 0;
7408 margin-bottom: -4px;
7409}
7410.windowContent {
7411 padding: 1px;
7412 max-height: calc(100vh - 115px);
7413 overflow: auto;
7414}
7415.windowBox {
7416 margin: 0;
7417}
7418.window.large .windowBox {
7419 flex-grow: 1;
7420}
7421.window:not(.hasTitle) .blockTitle {
7422 display: none;
7423}
7424.window:not(.hasTitle) .windowBox {
7425 padding-top: 10px;
7426}
7427.window:not(.large) .errorMsgBlock {
7428 position: relative;
7429 top: -7px;
7430}
7431.windowContent .element.withSomeError {
7432 margin-top: 23px;
7433}
7434
7435/* Форма поиска по глоссарию */
7436.glossarySearchForm {
7437 display: none !important;
7438 position: absolute;
7439 right: 1px;
7440 width: 400px;
7441 top: 32px;
7442 max-width: 80vw;
7443 background: #4a4e52;
7444 padding: 3px 5px 3px 26px;
7445 border-radius: 3px;
7446}
7447.btnGlossarySearch.active .glossarySearchForm {
7448 display: block !important;
7449}
7450.glossarySearchForm .gloss_button {
7451 display: none;
7452}
7453.glossarySearchForm .gloss_container .input_clear {
7454 top: 3px !important;
7455 font-size: 32px !important;
7456 left: -26px !important;
7457 width: 18px;
7458}
7459.glossarySearchForm .gloss_container .input_bordering {
7460 border: 1px solid #81c45600 !important;
7461 border-radius: 4px;
7462}
7463.glossarySearchForm .gloss_container .input_bordering input {
7464 margin: 0;
7465 font-size: 15px !important;
7466 width: 100% !important;
7467 padding-right: 8px !important;
7468}
7469.glossarySearchForm .gloss_container {
7470 visibility: visible !important;
7471 height: 30px;
7472 display: block;
7473 margin-bottom: -3px;
7474 width: 100% !important;
7475}
7476.darkSide .glossarySearchForm .gloss_container {
7477 margin-bottom: -1px;
7478}
7479
7480/* Виды (view) */
7481.view.hidden {
7482 position: absolute;
7483 z-index: -1;
7484 top: 0;
7485 visibility: hidden;
7486}
7487.view.hidden.excluded {
7488 display: none;
7489}
7490
7491/* actionButton */
7492.btn.actionButtonNoIcon a {
7493 padding-left: 12px;
7494}
7495.actionButtonNoIcon .linkButtonIconContainer {
7496 display: none;
7497}
7498
7499/* Воздушный шар (baloon) */
7500.baloon {
7501 border: 1px solid #81c456;
7502 padding: 10px;
7503 margin-top: 5px;
7504 background: #81c456;
7505 margin-bottom: 0 !important;
7506 border-radius: 5px;
7507}
7508.baloonTitle {
7509 font-size: 15px;
7510 font-weight: 600;
7511 margin: 0 0 5px;
7512}
7513.baloon.full {
7514 width: 100%;
7515}
7516
7517/* flexBox */
7518.flexBox {
7519 display: flex;
7520 justify-content: start;
7521 margin: -10px 0 0 -10px;
7522}
7523.flexBox>div {
7524 margin: 10px 0 0 10px !important;
7525 flex-grow: 10;
7526 min-width: 30%;
7527}
7528@media all and (max-width: 1000px) {
7529 .flexBox {
7530 flex-wrap: wrap;
7531 }
7532}
7533
7534/* infoLine */
7535.infoLine {
7536 line-height: 26px;
7537 display: flex;
7538}
7539.infoLine .ilTitle {
7540 display: inline-block;
7541 margin-right: 8px;
7542 font-size: 15px;
7543 flex-shrink: 0;
7544 max-width: 100%;
7545}
7546.infoLine .ilContent {
7547 font-weight: 600;
7548 white-space: pre-line;
7549 position: relative;
7550 top: 0px;
7551 word-break: break-word;
7552}
7553.infoLine.infoLines {
7554 flex-wrap: wrap;
7555}
7556.infoLine.infoLines .ilContent {
7557 font-weight: 400;
7558 margin-left: 10px;
7559}
7560.infoLine div:not(.infoLine) {
7561 line-height: initial;
7562}
7563
7564/* Спойлеры (Чендлер и Моника поженятся, Лея - сестра Люка, и всё в таком ключе) */
7565.spoiler.element .spoilerHead .btn {
7566 width: 100%;
7567 margin: 0;
7568}
7569.spoiler.element .spoilerHead .btn a {
7570 width: calc(100% - 44px);
7571 text-align: left;
7572 padding: 3px 12px 3px 28px;
7573 margin: 0;
7574}
7575.spoiler.element .spoilerHead .buttDiv {
7576 margin: 0;
7577}
7578.spoiler.element .spoilerHead .hotkeyHint {
7579 top: 4px;
7580 right: 7px;
7581}
7582.spoiler.element .spoilerHead .btn i {
7583 top: 2px;
7584 padding: 0;
7585 left: 4px;
7586 opacity: 0.8 !important;
7587 color: black;
7588}
7589.spoiler.element:not(.opened) .spoilerHead .btn i {
7590 transform: rotate(-90deg);
7591 top: 1px;
7592 left: 5px;
7593}
7594.spoiler.element.opened .spoilerHead .btn i, .spoiler.element:hover .spoilerHead .btn i {
7595 opacity: 1 !important;
7596}
7597.spoiler.element .spoilerContainer {
7598 max-height: 10000px;
7599 transition: all .3s cubic-bezier(1, 0.01, 0.68, 0.4) 0s;
7600}
7601.spoiler.element:not(.opened) .spoilerContainer {
7602 max-height: 0;
7603 overflow: hidden;
7604 transition: all .3s cubic-bezier(0.32, 0.63, 0.01, 1.01) 0s;
7605}
7606.spoiler.element:not(.opened) .spoilerHead .btn a {
7607 background-color: #FFFBEA;
7608}
7609.spoiler.element {
7610 transition: all .3s ease-out;
7611 padding: 8px;
7612 border: 1px solid #fff0;
7613}
7614.spoiler.element .spoilerContent {
7615 padding-top: 10px;
7616 white-space: pre-line;
7617}
7618.spoiler.element.opened {
7619 border: 1px solid #D5D9D3;
7620}
7621.darkSide .spoiler.element.opened {
7622 border-color: #4A4E52;
7623}
7624.darkSide .spoiler.element:not(.opened) .spoilerHead .btn:hover a {
7625 background: #585B5E;
7626}
7627.darkSide .spoiler.element:not(.opened) .spoilerHead .btn a {
7628 background: #E0DAC0;
7629}
7630.darkSide .spoiler.element .spoilerHead .btn:hover i, .darkSide .spoiler.element.opened .spoilerHead .btn i {
7631 color: #FFFBEA;
7632}
7633
7634/* Полноэкранный режим */
7635.fullScreenMode .task {
7636 padding: 0;
7637 overflow: hidden !important;
7638 margin: 0;
7639}
7640.fullScreenMode .task > *:not(.window):not(.customContextMenu) {
7641 /* Забыл уже, зщачем это.
7642 display: none;*/
7643}
7644.fullScreenMode .block.main {
7645 height: calc(100vh - 32px);
7646 width: 100%;
7647 margin: 0;
7648 padding: 0;
7649 border: 0 none;
7650 border-radius: 0px;
7651 display: block;
7652 margin-top: 32px;
7653 max-width: 100%;
7654 background: #f2f2f2;
7655}
7656.fullScreenMode .block.main .block {
7657 margin: 20px 10px 35px !important;
7658}
7659.fullScreenMode .block:not(.windowBox) {
7660 max-width: initial;
7661}
7662.darkSide.fullScreenMode .block.main {
7663 background: #090D12;
7664}
7665.fullScreenMode .block.main > .blockContent {
7666 overflow-x: hidden;
7667 height: 100%;
7668}
7669.fullScreenMode .block.main>.blockContent>.blockButtons, .fullScreenMode .block.main>.blockContent>.blockTitle {
7670 position: fixed;
7671 top: 0px;
7672 background: white;
7673 display: none;
7674}
7675.fullScreenMode .task_focused .block.main, .fullScreenMode .task_focused .block.main>.blockContent>.blockButtons, .fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7676 display: flex;
7677}
7678.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7679 box-shadow: 0 0 5px 0 #0005;
7680}
7681.fullScreenMode .block.main>.blockContent>.blockTitle {
7682 width: calc(100vw - 20px);
7683 margin: 0;
7684 border: 0;
7685 border-radius: 0;
7686 z-index: 1000;
7687 height: 32px;
7688 align-items: center;
7689 padding: 0 10px;
7690}
7691.fullScreenMode .block.main>.blockContent>.blockButtons {
7692 z-index: 1001;
7693 right: 10px;
7694 top: 2px;
7695}
7696.darkSide.fullScreenMode .block.main>.blockContent>.blockButtons, .darkSide.fullScreenMode .block.main>.blockContent>.blockTitle {
7697 color: #DCD8C7;
7698 background: #23272B;
7699}
7700.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7701 box-shadow: 0 0 5px 0 #0005;
7702}
7703.darkSide.fullScreenMode .task_focused .block.main>.blockContent>.blockTitle {
7704 box-shadow: 0 0 5px 0 #DCD8C750;
7705}
7706.fullScreenMode .task__error {
7707 top: 33px;
7708}
7709
7710/* Контекстное меню */
7711.customContextMenu {
7712 position: fixed;
7713 z-index: 9000;
7714}
7715.customContextMenuItemsContainer {
7716 border-radius: 5px;
7717 padding: 1px 2px 1px 1px;
7718 background: #808080;
7719 box-shadow: 3px 3px 18px 2px #0004;
7720}
7721.customContextMenuActionButtons {
7722 padding: 0;
7723 margin: 0;
7724}
7725.customContextMenuActionButtons .buttDiv {
7726 margin-right: 15px;
7727}
7728.customContextMenuActionButtons .btn {
7729 width: 100%;
7730 margin: 0px;
7731}
7732.customContextMenuActionButtons .btn a {
7733 width: 100%;
7734 margin: 1px;
7735 padding: 2px 7px;
7736 border: 0;
7737 border-radius: 0;
7738}
7739.customContextMenuRightClickField {
7740 border: 1px dashed #fffbea;
7741 border-radius: 5px 5px 0 0;
7742 margin: 1px 0 1px 1px;
7743 color: #fffbea;
7744 cursor: pointer;
7745 padding: 2px 7px;
7746}
7747.customContextMenuActionButtons .hotkeyHint {
7748 right: -13px;
7749 top: 0px;
7750}
7751
7752/* Мелочёвка и переопределения */
7753.leftPopup .popUpTitle {
7754 left: initial;
7755 right: 0;
7756}
7757
7758/* Мобильно-специфичные стили */
7759
7760@media all and (max-width: 768px) {
7761 .task {
7762 padding: 25px 0 10px 0;
7763 overflow: hidden;
7764 }
7765 .task:first-child {
7766 padding-top: 0;
7767 }
7768 .block {
7769 border-left: none !important;
7770 border-right: none !important;
7771 border-radius: 0 !important;
7772 padding: 10px;
7773 margin-top: 0;
7774 }
7775 .blockTitle {
7776 padding: 15px 12px !important;
7777 margin: 0;
7778 top: -12px;
7779 width: 100%;
7780 left: -10px;
7781 border-left: none !important;
7782 border-right: none !important;
7783 border-radius: 0 !important;
7784 position: relative;
7785 }
7786 .tabList {
7787 max-width: 96vw;
7788 left: 7vw;
7789 }
7790 textarea {
7791 min-height: 100px !important;
7792 }
7793 .element.focused:not(.imageField), .darkSide .element.focused:not(.imageField) {
7794 background: none;
7795 }
7796 .buttDiv {
7797 margin: 0 10px 10px 0;
7798 padding: 0;
7799 }
7800 .btn {
7801 margin: 0;
7802 }
7803 .btn.active {
7804 margin: 0 !important;
7805 }
7806 .btn a {
7807 min-height: 19px;
7808 margin: 0;
7809 padding: 7px 15px 6px;
7810 vertical-align: baseline !important;
7811 }
7812 .btn.linkButton a {
7813 padding-left: 30px;
7814 }
7815 .btn.linkButton.actionButtonNoIcon a {
7816 padding-left: 16px;
7817 }
7818 .linkButtonIconContainer {
7819 top: 6px;
7820 }
7821 .popUpTitle {
7822 display: none !important;
7823 }
7824 .hotkeyHint {
7825 display: none;
7826 }
7827 .switchButton>i {
7828 top: 6px;
7829 }
7830 .stepTitle.title {
7831 margin: 0 0 10px 0;
7832 }
7833 input {
7834 min-height: 42px;
7835 }
7836 .btn:hover a, .darkSide .btn:hover a {
7837 box-shadow: none;
7838 }
7839 .blockContent>.buttDiv {
7840 margin-left: 9px;
7841 }
7842 .blockButtons .blockTitle {
7843 padding: 2px 10px !important;
7844 margin: 0 !important;
7845 }
7846 .blockButtons {
7847 position: relative;
7848 top: -11px;
7849 width: 100vw;
7850 left: -10px;
7851 flex-wrap: wrap;
7852 }
7853 .blockButtons .blockTitle.tabScroller {
7854 width: max-content;
7855 text-align: right;
7856 padding-top: 7px !important;
7857 flex-grow: 10;
7858 }
7859 .blockButtons .blockTitle.custom {
7860 order: 10;
7861 border-top: 0 none;
7862 padding: 5px 12px !important;
7863 width: 100%;
7864 }
7865 .glossarySearchForm {
7866 width: 100vw;
7867 position: fixed;
7868 left: 0;
7869 max-width: 100vw;
7870 }
7871 .glossarySearchForm .gloss_container .input_bordering {
7872 border-radius: 0;
7873 border-left: 0 !important;
7874 border-right: 0 !important;
7875 box-shadow: -2px 3px 9px 0px #0008;
7876 }
7877 .imageField .btn.linkButton a {
7878 padding-left: 12px;
7879 padding-top: 3px;
7880 height: 19px;
7881 }
7882 .checkboxGroup:not(.asButtons) .btn a:before, .radioGroup:not(.asButtons) .btn a:before {
7883 border: 1px solid #d9d9d9;
7884 }
7885 .errorMsgBlock:after {
7886 border-color:transparent;
7887 border-top-color: rgb(250, 112, 103);
7888 }
7889}
7890
7891";
7892
7893
7894
7895
7896
7897
7898
7899$('head').append('<meta name="referrer" content="no-referrer" />');
7900
7901document.ready = new Promise(resolve => {
7902
7903 if (document.readyState === 'complete') {
7904 resolve();
7905 } else {
7906 function onReady() {
7907 resolve();
7908 document.removeEventListener('DOMContentLoaded', onReady, true);
7909 window.removeEventListener('load', onReady, true);
7910 }
7911
7912 document.addEventListener('DOMContentLoaded', onReady, true);
7913 window.addEventListener('load', onReady, true);
7914 }
7915});
7916
7917function extend(ParentClass, constructorFunction, prototypeHash) {
7918 constructorFunction = constructorFunction || function () {
7919 };
7920 prototypeHash = prototypeHash || {};
7921 if (ParentClass) {
7922 constructorFunction.prototype = Object.create(ParentClass.prototype);
7923 }
7924 for (const i in prototypeHash) {
7925 constructorFunction.prototype[i] = prototypeHash[i];
7926 }
7927 return constructorFunction;
7928}