vimp-engine 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +11 -4
  2. package/src/client/InputListener.js +36 -0
  3. package/src/client/SoundManager.js +498 -0
  4. package/src/client/boot.js +113 -0
  5. package/src/client/components/controller/Auth.js +44 -0
  6. package/src/client/components/controller/CanvasManager.js +26 -0
  7. package/src/client/components/controller/Chat.js +56 -0
  8. package/src/client/components/controller/Controls.js +51 -0
  9. package/src/client/components/controller/Game.js +37 -0
  10. package/src/client/components/controller/Lobby.js +81 -0
  11. package/src/client/components/controller/LobbyAuth.js +41 -0
  12. package/src/client/components/controller/Panel.js +23 -0
  13. package/src/client/components/controller/Stat.js +33 -0
  14. package/src/client/components/controller/Vote.js +55 -0
  15. package/src/client/components/model/Auth.js +92 -0
  16. package/src/client/components/model/CanvasManager.js +319 -0
  17. package/src/client/components/model/Chat.js +119 -0
  18. package/src/client/components/model/Controls.js +120 -0
  19. package/src/client/components/model/Game.js +172 -0
  20. package/src/client/components/model/Lobby.js +226 -0
  21. package/src/client/components/model/LobbyAuth.js +161 -0
  22. package/src/client/components/model/Panel.js +70 -0
  23. package/src/client/components/model/Stat.js +77 -0
  24. package/src/client/components/model/Vote.js +216 -0
  25. package/src/client/components/view/Auth.js +167 -0
  26. package/src/client/components/view/CanvasManager.js +44 -0
  27. package/src/client/components/view/Chat.js +90 -0
  28. package/src/client/components/view/Controls.js +30 -0
  29. package/src/client/components/view/Game.js +48 -0
  30. package/src/client/components/view/Lobby.js +303 -0
  31. package/src/client/components/view/LobbyAuth.js +111 -0
  32. package/src/client/components/view/Panel.js +314 -0
  33. package/src/client/components/view/Stat.js +238 -0
  34. package/src/client/components/view/Vote.js +108 -0
  35. package/src/client/debug.js +158 -0
  36. package/src/client/lib/autostart.js +61 -0
  37. package/src/client/lib/contextTracker.js +46 -0
  38. package/src/client/lib/formBuilder.js +280 -0
  39. package/src/client/lib/hostGate.js +16 -0
  40. package/src/client/main.js +2016 -0
  41. package/src/client/network/HostConnectionManager.js +196 -0
  42. package/src/client/network/HostController.js +422 -0
  43. package/src/client/network/InlineHostBridge.js +152 -0
  44. package/src/client/network/LoopbackTransport.js +51 -0
  45. package/src/client/network/SignalingClient.js +146 -0
  46. package/src/client/network/WebRtcManager.js +147 -0
  47. package/src/client/network/WebSocketTransport.js +78 -0
  48. package/src/client/network/policyClose.js +54 -0
  49. package/src/client/network/workerSupport.js +34 -0
  50. package/src/client/providers/BakingProvider.js +88 -0
  51. package/src/client/providers/DependencyProvider.js +41 -0
  52. package/src/client/style.css +898 -0
  53. package/src/client/views/gameShell.js +190 -0
  54. package/src/client/views/includes/auth.pug +20 -0
  55. package/src/client/views/includes/chat.pug +5 -0
  56. package/src/client/views/includes/informer.pug +2 -0
  57. package/src/client/views/includes/lobby.pug +48 -0
  58. package/src/client/views/includes/lobbyAuth.pug +19 -0
  59. package/src/client/views/includes/panel.pug +3 -0
  60. package/src/client/views/includes/stat.pug +2 -0
  61. package/src/client/views/index.pug +8 -0
  62. package/src/config/closeCodes.js +18 -0
  63. package/src/config/env.js +61 -0
  64. package/src/devtools/ScenarioRunner.js +3 -8
  65. package/src/devtools/pluginLoader.js +8 -93
  66. package/src/host/HostGame.js +41 -3
  67. package/src/host/PortMachine.js +294 -0
  68. package/src/host/host.worker.js +29 -246
  69. package/src/host/identity.js +100 -0
  70. package/src/lib/clientIp.js +39 -0
  71. package/src/lib/loadGamePackage.js +122 -0
  72. package/src/lib/offlinePlayerData.js +18 -0
  73. package/src/standalone/index.js +125 -0
  74. package/tests/fixtures/miniGame/config/game.js +4 -1
@@ -0,0 +1,314 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+ import { lerp } from '../../../lib/math.js';
3
+
4
+ // Singleton PanelView
5
+
6
+ let panelView;
7
+
8
+ // дефолты bar-поля (могут переопределяться схемой игры)
9
+ const DEFAULT_BAR_BLOCKS = 30;
10
+ const DEFAULT_BAR_MAX = 100;
11
+ const EMPTY_BLOCK_COLOR = '#888';
12
+ const BAR_FILL_DELAY_MS = 500;
13
+ const BAR_FILL_DURATION_MS = 500;
14
+
15
+ // системная настройка «уменьшить движение» — округление в один момент
16
+ // времени: matchMedia недоступен в тестовой среде без happy-dom-полифилла
17
+ const prefersReducedMotion = () =>
18
+ typeof matchMedia === 'function' &&
19
+ matchMedia('(prefers-reduced-motion: reduce)').matches;
20
+
21
+ export default class PanelView {
22
+ // config — { containerId (движок), fields (схема игры: массив
23
+ // { name, elem, type: 'bar'|'value'|'time'|'weapon', max?, blocks? }) }
24
+ // Семантику ячейки задаёт type, а не имя поля: игра с полем `energy`
25
+ // вместо `health` получает тот же бар
26
+ constructor(model, config) {
27
+ if (panelView) {
28
+ return panelView;
29
+ }
30
+
31
+ panelView = this;
32
+
33
+ const { containerId, fields } = config;
34
+
35
+ this._panels = {};
36
+ this._fields = {};
37
+ this._bars = {};
38
+ this._weaponList = fields
39
+ .filter(field => field.type === 'weapon')
40
+ .map(field => field.name);
41
+
42
+ // DOM панели генерируется по схеме игры в порядке fields
43
+ this._buildPanel(document.getElementById(containerId), fields);
44
+
45
+ this.publisher = new Publisher();
46
+
47
+ this._mPublic = model.publisher;
48
+ this._mPublic.on('data', 'update', this);
49
+ this._mPublic.on('hide', 'hidePanel', this);
50
+ this._mPublic.on('activeWeapon', 'setCurrentWeapon', this);
51
+ }
52
+
53
+ // генерирует таблицу панели по схеме игры (замена хардкода panel.pug)
54
+ _buildPanel(container, fields) {
55
+ const table = document.createElement('table');
56
+ const row = table.insertRow(-1);
57
+
58
+ for (const field of fields) {
59
+ const cell = row.insertCell(-1);
60
+
61
+ cell.setAttribute('id', field.elem);
62
+ this._panels[field.name] = cell;
63
+ this._fields[field.name] = field;
64
+
65
+ if (field.type === 'bar') {
66
+ this._initBar(field, cell);
67
+ }
68
+ }
69
+
70
+ container.appendChild(table);
71
+ }
72
+
73
+ // инициализирует полосу-бар поля type: 'bar'
74
+ _initBar(field, cell) {
75
+ cell.textContent = '';
76
+
77
+ const wrapper = document.createElement('div');
78
+
79
+ wrapper.className = 'panel-bar-wrapper';
80
+
81
+ const total = field.blocks ?? DEFAULT_BAR_BLOCKS;
82
+ const bar = {
83
+ max: field.max ?? DEFAULT_BAR_MAX,
84
+ total,
85
+ blocks: [],
86
+ colors: [],
87
+ // целевое состояние текущего раунда: { blocksToShow, blink } как
88
+ // только пришёл хотя бы один update() после playRoundStart, иначе
89
+ // null — этим отличаем «данные ещё не пришли» от «пришли нулевые»
90
+ pendingTarget: null,
91
+ // true пока идёт заполнение слева направо в начале раунда — обычные
92
+ // update() в это время не должны перерисовывать бар мгновенно
93
+ animating: false,
94
+ // id таймеров текущего заполнения — отменяются при повторном
95
+ // playRoundStart, чтобы наложение двух запусков не гонялось за DOM
96
+ delayTimer: null,
97
+ fillTimer: null,
98
+ blockTimers: [],
99
+ };
100
+
101
+ for (let i = 0; i < total; i += 1) {
102
+ const block = document.createElement('div');
103
+
104
+ block.className = 'panel-bar-block';
105
+ block.style.backgroundColor = EMPTY_BLOCK_COLOR;
106
+ block.textContent = ' '; // неразрывный пробел
107
+
108
+ wrapper.appendChild(block);
109
+
110
+ bar.blocks.push(block);
111
+ bar.colors.push(this.getBarBlockColor(i, total));
112
+ }
113
+
114
+ this._bars[field.name] = bar;
115
+ cell.appendChild(wrapper);
116
+ }
117
+
118
+ // вычисляет цвет блока бара на основе его индекса
119
+ getBarBlockColor(index, total) {
120
+ const progress = index / (total - 1);
121
+
122
+ const colors = [
123
+ { p: 0, c: { r: 255, g: 50, b: 50 } }, // red
124
+ { p: 0.25, c: { r: 255, g: 165, b: 0 } }, // orange
125
+ { p: 0.5, c: { r: 255, g: 255, b: 0 } }, // yellow
126
+ { p: 0.75, c: { r: 50, g: 205, b: 50 } }, // green
127
+ { p: 1, c: { r: 0, g: 220, b: 220 } }, // cyan
128
+ ];
129
+
130
+ let start, end;
131
+
132
+ for (let i = 1, len = colors.length; i < len; i += 1) {
133
+ if (progress <= colors[i].p) {
134
+ start = colors[i - 1];
135
+ end = colors[i];
136
+ break;
137
+ }
138
+ }
139
+
140
+ const localProgress = (progress - start.p) / (end.p - start.p);
141
+ const r = Math.round(lerp(start.c.r, end.c.r, localProgress));
142
+ const g = Math.round(lerp(start.c.g, end.c.g, localProgress));
143
+ const b = Math.round(lerp(start.c.b, end.c.b, localProgress));
144
+
145
+ return `rgb(${r}, ${g}, ${b})`;
146
+ }
147
+
148
+ // обновляет пользовательскую панель
149
+ update(data) {
150
+ const { name, value } = data;
151
+ const elem = this._panels[name];
152
+
153
+ if (this._fields[name]?.type === 'bar') {
154
+ this._updateBar(name, value);
155
+ } else {
156
+ elem.textContent = value;
157
+ }
158
+
159
+ elem.style.display = 'table-cell';
160
+ }
161
+
162
+ // перерисовывает бар по текущему значению
163
+ _updateBar(name, value) {
164
+ const bar = this._bars[name];
165
+ const exactBlocks = (value / bar.max) * bar.total;
166
+ const blocksToShow = Math.ceil(exactBlocks);
167
+ const blink = value > 0 && exactBlocks % 1 !== 0;
168
+
169
+ // запоминаем целевое состояние текущего раунда — из него собирается
170
+ // заполнение слева направо в начале раунда
171
+ bar.pendingTarget = { blocksToShow, blink };
172
+
173
+ // пока идёт заполнение бара в начале раунда, не перерисовываем его
174
+ // мгновенно — иначе повторный update() (например, эхо того же
175
+ // значения) перебьёт постепенное заполнение
176
+ if (!bar.animating) {
177
+ this._paintBar(name, blocksToShow, blink);
178
+ }
179
+ }
180
+
181
+ // отрисовывает бар мгновенно: blocksToShow закрашенных блоков слева,
182
+ // остальные — пустые; blink — мигание последнего неполного блока
183
+ _paintBar(name, blocksToShow, blink) {
184
+ const { blocks, colors } = this._bars[name];
185
+
186
+ blocks.forEach((block, index) => {
187
+ if (index < blocksToShow) {
188
+ block.className = 'panel-bar-block';
189
+ block.style.backgroundColor = colors[index];
190
+ } else {
191
+ block.className = 'panel-bar-block-empty';
192
+ block.style.backgroundColor = EMPTY_BLOCK_COLOR;
193
+ }
194
+ });
195
+
196
+ if (blink) {
197
+ blocks[blocksToShow - 1].classList.add('panel-bar-blink');
198
+ }
199
+ }
200
+
201
+ // отменяет все таймеры незавершённого заполнения бара — вызывается перед
202
+ // повторным запуском, чтобы наложение двух playRoundStart не гонялось
203
+ // за одним и тем же DOM
204
+ _cancelBarTimers(bar) {
205
+ if (bar.delayTimer !== null) {
206
+ clearTimeout(bar.delayTimer);
207
+ bar.delayTimer = null;
208
+ }
209
+
210
+ if (bar.fillTimer !== null) {
211
+ clearTimeout(bar.fillTimer);
212
+ bar.fillTimer = null;
213
+ }
214
+
215
+ for (const timer of bar.blockTimers) {
216
+ clearTimeout(timer);
217
+ }
218
+
219
+ bar.blockTimers = [];
220
+ }
221
+
222
+ // заполняет бар слева направо блок за блоком за BAR_FILL_DURATION_MS, до
223
+ // целевого значения текущего раунда (bar.pendingTarget)
224
+ _animateBarFill(name) {
225
+ const bar = this._bars[name];
226
+ const { blocks, colors, pendingTarget } = bar;
227
+
228
+ if (!pendingTarget) {
229
+ // PS_PANEL_DATA текущего раунда ещё не пришёл — заполнение до
230
+ // устаревшего значения прошлого раунда хуже, чем его отсутствие;
231
+ // ближайший update() отрисует актуальное значение мгновенно
232
+ bar.animating = false;
233
+ return;
234
+ }
235
+
236
+ const { blocksToShow, blink } = pendingTarget;
237
+ const step =
238
+ blocksToShow > 0 ? BAR_FILL_DURATION_MS / blocksToShow : 0;
239
+
240
+ for (let index = 0; index < blocksToShow; index += 1) {
241
+ bar.blockTimers.push(
242
+ setTimeout(() => {
243
+ blocks[index].className = 'panel-bar-block';
244
+ blocks[index].style.backgroundColor = colors[index];
245
+
246
+ if (blink && index === blocksToShow - 1) {
247
+ blocks[index].classList.add('panel-bar-blink');
248
+ }
249
+ }, step * index),
250
+ );
251
+ }
252
+
253
+ bar.fillTimer = setTimeout(() => {
254
+ bar.fillTimer = null;
255
+ bar.animating = false;
256
+
257
+ // финальная сверка: если во время заполнения пришло более новое
258
+ // значение (например, урон), отрисовать его, а не зафиксированную
259
+ // в начале заполнения цель
260
+ const target = bar.pendingTarget;
261
+
262
+ this._paintBar(name, target.blocksToShow, target.blink);
263
+ }, step * blocksToShow);
264
+ }
265
+
266
+ hidePanel(name) {
267
+ this._panels[name].style.display = 'none';
268
+ }
269
+
270
+ // с задержкой BAR_FILL_DELAY_MS заполняет bar-поля (например, здоровье)
271
+ // слева направо за BAR_FILL_DURATION_MS: к этому моменту актуальное
272
+ // значение обычно уже пришло через PS_PANEL_DATA (см. порядок отправки в
273
+ // RoundManager._startRound), а если ещё нет — заполнение просто
274
+ // пропускается (см. _animateBarFill). Анимация лого — забота main.js
275
+ // (лого вне containerId панели, см. playLogoRoundStart)
276
+ playRoundStart() {
277
+ const reducedMotion = prefersReducedMotion();
278
+
279
+ for (const name of Object.keys(this._bars)) {
280
+ const bar = this._bars[name];
281
+
282
+ this._cancelBarTimers(bar);
283
+ bar.pendingTarget = null;
284
+
285
+ if (reducedMotion) {
286
+ // без поблочного заполнения — ближайший update() отрисует
287
+ // значение сразу
288
+ bar.animating = false;
289
+ continue;
290
+ }
291
+
292
+ bar.animating = true;
293
+ this._paintBar(name, 0, false);
294
+
295
+ bar.delayTimer = setTimeout(() => {
296
+ bar.delayTimer = null;
297
+ this._animateBarFill(name);
298
+ }, BAR_FILL_DELAY_MS);
299
+ }
300
+ }
301
+
302
+ // устанавливает активное оружие
303
+ setCurrentWeapon(activeWeaponName) {
304
+ for (const weaponName of this._weaponList) {
305
+ const elem = this._panels[weaponName];
306
+
307
+ if (weaponName === activeWeaponName) {
308
+ elem.classList.add('active');
309
+ } else {
310
+ elem.classList.remove('active');
311
+ }
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,238 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+
3
+ // Singleton StatView
4
+
5
+ let statView;
6
+
7
+ export default class StatView {
8
+ // config — { elems (движок: контейнер), params (схема игры: columns —
9
+ // подписи колонок, bodies — таблицы команд) }
10
+ constructor(model, config) {
11
+ if (statView) {
12
+ return statView;
13
+ }
14
+
15
+ statView = this;
16
+
17
+ const { elems, params } = config;
18
+
19
+ this._stat = document.getElementById(elems.stat);
20
+
21
+ // DOM scoreboard генерируется по схеме игры: произвольное число
22
+ // команд (bodies) и колонок (columns)
23
+ this._buildStat(params);
24
+
25
+ this.publisher = new Publisher();
26
+
27
+ this._mPublic = model.publisher;
28
+
29
+ this._mPublic.on('open', 'open', this);
30
+ this._mPublic.on('close', 'close', this);
31
+ this._mPublic.on('tHead', 'updateTableHead', this);
32
+ this._mPublic.on('tBody', 'updateTableBody', this);
33
+ this._mPublic.on('clearBodies', 'clearBodies', this);
34
+ }
35
+
36
+ // генерирует шапку и таблицы по схеме (замена хардкода stat.pug)
37
+ _buildStat(params) {
38
+ const { columns, bodies } = params;
39
+
40
+ const head = document.createElement('div');
41
+
42
+ head.className = 'stat-head';
43
+
44
+ for (const label of columns) {
45
+ const span = document.createElement('span');
46
+
47
+ span.textContent = label;
48
+ head.appendChild(span);
49
+ }
50
+
51
+ const tables = document.createElement('div');
52
+
53
+ tables.className = 'stat-tables';
54
+
55
+ // порядок таблиц — по числовым ключам bodies (id команд)
56
+ const tableNames = Object.keys(bodies)
57
+ .sort((a, b) => a - b)
58
+ .map(key => bodies[key]);
59
+
60
+ for (const name of tableNames) {
61
+ const table = document.createElement('table');
62
+
63
+ table.setAttribute('id', name);
64
+
65
+ const tHead = table.createTHead();
66
+ const headRow = tHead.insertRow(-1);
67
+
68
+ for (let i = 0, len = columns.length; i < len; i += 1) {
69
+ headRow.insertCell(-1);
70
+ }
71
+
72
+ table.appendChild(document.createElement('tbody'));
73
+ tables.appendChild(table);
74
+ }
75
+
76
+ this._stat.appendChild(head);
77
+ this._stat.appendChild(tables);
78
+ }
79
+
80
+ // открывает статистику
81
+ open() {
82
+ this._stat.style.display = 'block';
83
+ }
84
+
85
+ // закрывает статистику
86
+ close() {
87
+ this._stat.style.display = 'none';
88
+ }
89
+
90
+ // очищает таблицы <tbody>
91
+ clearBodies(bodiesList) {
92
+ for (let i = 0, len = bodiesList.length; i < len; i += 1) {
93
+ const table = document.getElementById(bodiesList[i]);
94
+ const tBodies = table.tBodies;
95
+
96
+ for (let i2 = 0, len2 = tBodies.length; i2 < len2; i2 += 1) {
97
+ tBodies[i2].textContent = '';
98
+ }
99
+ }
100
+ }
101
+
102
+ // обновляет <thead>
103
+ updateTableHead(data) {
104
+ const table = document.getElementById(data.tableId);
105
+ const cells = table.tHead.rows[data.rowNumber].cells;
106
+ const cellsData = data.cellsData;
107
+
108
+ for (let i = 0, len = cells.length; i < len; i += 1) {
109
+ cells[i].textContent = cellsData[i];
110
+ }
111
+ }
112
+
113
+ // обновляет <tbody>
114
+ updateTableBody(data) {
115
+ const table = document.getElementById(data.tableId);
116
+ const tbody = table.tBodies[data.bodyNumber];
117
+ let row = tbody.rows.namedItem(`stat_${data.id}`);
118
+ const { cellsData, sortData } = data;
119
+
120
+ // сортирует
121
+ const sorting = rowIndex => {
122
+ const row = tbody.rows[rowIndex];
123
+ const prevRow = tbody.rows[rowIndex - 1];
124
+ const nextRow = tbody.rows[rowIndex + 1];
125
+
126
+ // если есть предыдущая строка
127
+ if (prevRow) {
128
+ for (let i = 0, len = sortData.length; i < len; i += 1) {
129
+ const number = sortData[i][0];
130
+ const type = sortData[i][1];
131
+ const value = ~~row.cells[number].textContent;
132
+ const prevValue = ~~prevRow.cells[number].textContent;
133
+
134
+ // если type == true, значит сортировка по убыванию
135
+ if (type) {
136
+ // если предыдущее значение меньше текущего
137
+ if (prevValue < value) {
138
+ tbody.insertBefore(row, prevRow);
139
+ sorting(rowIndex - 1);
140
+ return;
141
+ }
142
+
143
+ if (prevValue > value) {
144
+ break;
145
+ }
146
+
147
+ // иначе сортировка по возрастанию
148
+ } else {
149
+ // если предыдущее значение больше текущего
150
+ if (prevValue > value) {
151
+ tbody.insertBefore(row, prevRow);
152
+ sorting(rowIndex - 1);
153
+ return;
154
+ }
155
+
156
+ if (prevValue < value) {
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // если есть следующая строка
164
+ if (nextRow) {
165
+ for (let i = 0, len = sortData.length; i < len; i += 1) {
166
+ const number = sortData[i][0];
167
+ const type = sortData[i][1];
168
+ const value = ~~row.cells[number].textContent;
169
+ const nextValue = ~~nextRow.cells[number].textContent;
170
+
171
+ // если type == true, значит сортировка по убыванию
172
+ if (type) {
173
+ // если следующее значение больше текущего
174
+ if (nextValue > value) {
175
+ tbody.insertBefore(nextRow, row);
176
+ sorting(rowIndex + 1);
177
+ return;
178
+ }
179
+
180
+ if (nextValue < value) {
181
+ break;
182
+ }
183
+
184
+ // иначе сортировка по возрастанию
185
+ } else {
186
+ // если следующее значение меньше текущего
187
+ if (nextValue < value) {
188
+ tbody.insertBefore(nextRow, row);
189
+ sorting(rowIndex + 1);
190
+ return;
191
+ }
192
+
193
+ if (nextValue > value) {
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ };
200
+
201
+ // если строка отсутствует
202
+ if (row === null) {
203
+ // если есть данные для создания строки, создать ее
204
+ if (cellsData !== null) {
205
+ row = tbody.insertRow(-1);
206
+ row.setAttribute('id', `stat_${data.id}`);
207
+
208
+ for (let i = 0, len = cellsData.length; i < len; i += 1) {
209
+ const cell = row.insertCell(i);
210
+ cell.textContent = cellsData[i];
211
+ }
212
+
213
+ if (sortData) {
214
+ sorting(row.sectionRowIndex);
215
+ }
216
+ }
217
+
218
+ // иначе, если строка присутствует
219
+ } else {
220
+ // если данные строки === null, удалить строку
221
+ if (cellsData === null) {
222
+ row.parentNode.removeChild(row);
223
+
224
+ // иначе обновить строку
225
+ } else {
226
+ const cells = row.cells;
227
+
228
+ for (let i = 0, len = cells.length; i < len; i += 1) {
229
+ cells[i].textContent = cellsData[i];
230
+ }
231
+
232
+ if (sortData) {
233
+ sorting(row.sectionRowIndex);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,108 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+
3
+ // Singleton VoteView
4
+
5
+ let voteView;
6
+
7
+ export default class VoteView {
8
+ constructor(model, elems, container = document.body) {
9
+ if (voteView) {
10
+ return voteView;
11
+ }
12
+
13
+ voteView = this;
14
+
15
+ // окно создаётся в рантайме, поэтому точка монтирования приходит извне:
16
+ // в solo это контейнер SDK, иначе body (см. main.js, точка ветвления 5)
17
+ this._container = container;
18
+
19
+ this._voteId = elems.voteId;
20
+ this._titleClass = elems.titleClass;
21
+ this._listClass = elems.listClass;
22
+ this._navClass = elems.navClass;
23
+ this._navActiveClass = elems.navActiveClass;
24
+
25
+ this.publisher = new Publisher();
26
+
27
+ this._mPublic = model.publisher;
28
+
29
+ this._mPublic.on('vote', 'createVote', this);
30
+ this._mPublic.on('clear', 'removeVote', this);
31
+ }
32
+
33
+ // создает окно голосования
34
+ createVote(data) {
35
+ const { title, list, back, more, time } = data;
36
+ const vote = document.createElement('div');
37
+ const p = document.createElement('p');
38
+ const ol = document.createElement('ol');
39
+ const navContainer = document.createElement('div');
40
+ const backElem = document.createElement('p');
41
+ const moreElem = document.createElement('p');
42
+ const exitElem = document.createElement('p');
43
+ let timerId = null;
44
+
45
+ vote.setAttribute('id', this._voteId);
46
+
47
+ p.setAttribute('class', this._titleClass);
48
+ p.textContent = title;
49
+
50
+ ol.setAttribute('class', this._listClass);
51
+
52
+ list.forEach(item => {
53
+ const li = document.createElement('li');
54
+ li.textContent = item;
55
+ ol.appendChild(li);
56
+ });
57
+
58
+ vote.appendChild(p);
59
+ vote.appendChild(ol);
60
+
61
+ navContainer.setAttribute('class', this._navClass);
62
+
63
+ if (back) {
64
+ backElem.setAttribute('class', this._navActiveClass);
65
+ }
66
+
67
+ if (more) {
68
+ moreElem.setAttribute('class', this._navActiveClass);
69
+ }
70
+
71
+ exitElem.setAttribute('class', this._navActiveClass);
72
+
73
+ backElem.textContent = '8. Back';
74
+ moreElem.textContent = '9. More';
75
+ exitElem.textContent = '0. Exit';
76
+
77
+ navContainer.appendChild(backElem);
78
+ navContainer.appendChild(moreElem);
79
+ navContainer.appendChild(exitElem);
80
+
81
+ vote.appendChild(navContainer);
82
+
83
+ this._container.appendChild(vote);
84
+
85
+ if (time !== null) {
86
+ timerId = setTimeout(() => {
87
+ vote.parentElement.removeChild(vote);
88
+ this.publisher.emit('timer', null);
89
+ this.publisher.emit('clear');
90
+ }, time);
91
+ }
92
+
93
+ this.publisher.emit('timer', timerId);
94
+ }
95
+
96
+ // удаляет окно голосования
97
+ removeVote(timerId) {
98
+ const vote = document.getElementById(this._voteId);
99
+
100
+ if (timerId) {
101
+ clearTimeout(timerId);
102
+ }
103
+
104
+ if (vote) {
105
+ vote.parentElement.removeChild(vote);
106
+ }
107
+ }
108
+ }