newhelper-js 2.1.6 → 2.1.8

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 (4) hide show
  1. package/README.md +29 -17
  2. package/history.md +30 -5
  3. package/newHelper.js +1464 -1103
  4. package/package.json +5 -2
package/newHelper.js CHANGED
@@ -1,1136 +1,1497 @@
1
-
2
1
  /*
3
- * Перед вами код newHelper.js версии 2.1.6, он построен на базе фабрики
4
- * Которая начинается с Intl.newHelper=function(){...};
2
+ * Перед вами код newHelper.js версии 2.1.8,
3
+ * Фронтенд библиотека сверхлегких и мощных утилит.
4
+ * Библиотека построена на базе фабрики
5
+ * Которая начинается с Intl.newHelper=function(namespace){...};
5
6
  * Причина использоватся Intl.newHelper банально проста
6
- * Если я сейчас засираю глобалскоуп одной полу гибкой переменной
7
- * То почему бы не начать отказываться от засирания глобал скоупа как такового
7
+ * Если я в 2.1.0 засирал глобалскоуп одной полу гибкой переменной
8
+ * И парочкой addEventListener,
9
+ * То почему бы не начать отказываться от засирания глобал скоупа
10
+ * при скачивании библиотеки как такового
8
11
  * И да, для инициализации ньюхелпера реально нужно писать
9
- * yourVariable = Intl.newHelper()
12
+ * Intl.newHelper(yourVar) чтобы newHelper появился в window.yourVar
10
13
  *
11
14
  * Стиль комментариев
12
15
  * FIXME - странное поведение функции, которое желательно бы переделать
13
- * ??? - требует уточнения
16
+ * ну или просто заметки для себя на будущее
17
+ * HMM - требует уточнения
14
18
  * !!! - обратите внимание
15
19
  * See also - почитайте для понимания как устроено
20
+ *
16
21
  *
17
- * ???: рассмотреть переход на es6 экспорт вместо вкладывания фабрики в Intl
22
+ * HMM: рассмотреть переход на es6 экспорт вместо вкладывания фабрики в Intl
23
+ * HMM: рассмотреть переделку окон под iife фабрику
18
24
  *
25
+ * Модули пришедшие с релизом 2.1:
26
+ * link
27
+ * lazy
28
+ * lang
29
+ * http
30
+ * html
31
+ * storage
32
+ * err
33
+ * hotkeys
34
+ * win (+wins)
35
+ *
36
+ * Модули удалённые после 2.1 (ищите полифиллы в конце файла):
37
+ * $
38
+ *
19
39
  * Новые модули, готовятсяк релизу в 2.2
20
- * их апи может быть чуть чуть нестабильно:
21
- * tables - на стадии рефакторинга
40
+ * их апи может быть чуть чуть нестабильно
41
+ * link (пропатченный, см. window[_].link.get => dynamic)
22
42
  * form
23
- * drag (портирован из окон)
24
43
  * pipe/pipeAsync
25
- * В 2.2 появятся плагины, некоторые модули ядра будут вынесены в плагины:
26
- * win (в 2.2)
27
- * tables (возможно он не будет частью ядра, но с другой стороны он полезен в админках)
28
- * http (в 2.3)
44
+ * drag (портирован из win._initDrag)
45
+ * fade (портирован из win._animate)
46
+ * x10 (x window system - портирован и рефакторнут из кусков win)
47
+ * toast
48
+
29
49
  *
30
50
  * Модули, имена которых зарезервированы на 2.3++
31
51
  * не используйте их неймспейсы для плагинов:
32
- * toast
33
52
  * filezone
34
53
  * ikarus
54
+ * tables
55
+ * resize
35
56
  *
36
57
  * Плагины, новый паттерн который я хочу узаконить в 2.2
37
58
  * Это не _.use(), не _.plugins, не мутация прототипа
38
- * Простое назначение _.myPlugin = pluginFabric();
59
+ * Простое назначение _.myPlugin = pluginFabric(_);
39
60
  * (где _ это уже вызванная фабрика ядра)
40
- * Как предполагается работать? также как и Intl.newHelper()
61
+ * Как предполагается работать? также как и Intl.newHelper(namespace)
41
62
  * Фабрика плагина возвращает объект, метод, или класс, или что вам нужно
42
63
  * Вам для подключения плагина просто нужно дать плагину неймспейс внутри ядра
43
64
  * И вызвать фабрику, всё!
65
+ *
66
+ * О модуле таблиц!!!!
67
+ * Я его удалил потому что создавать второй движок окон мне нахуй не надо
68
+ * Эта блядота заняла бы у меня ещё порядка 200-400 строк просто чтобы стать
69
+ * "хорошей альтернативой" условным react-tables или datatables
70
+ * Если я и захочу его делать снова то ждите 2.4, может быть тогда у меня хватит ума
71
+ * Придумать как сделать правильно, и по своему
72
+ * А щас, пусть эта гнида горит в аду, не место недопиленному говну в ядре unix.js
73
+ *
74
+ * А если вам нужны именно ньюхелпер таблицы
75
+ * Будьте добры проверить исходники 2.1.6 на npm
76
+ * Или пилите самодельные таблицы через innerHTML или window[_].html
77
+ * Что вам удобнее то и берите
78
+ *
79
+ * Я (MIOBOMB) хочу релизнуть 2.2 уже после 2.1.8,
80
+ * ибо мне в идеале закончить тосты и Object Hub 0.98
44
81
  */
45
82
 
46
- Intl.newHelper=function() {let _ = {
47
- link: {
48
- /*
49
- * МОДУЛЬ ССЫЛОК
50
- *
51
- * Работает по принципу [ссылка, команды...]
52
- * Пример: ?home&debug&lang=ru
53
- * ^^^^ ^^^^^^^^^^^^^
54
- * страница команды
55
- *
56
- * В процессе разработки ядра 2.0 в Object hub я понял
57
- * Что команды могут быть очень полезными для отладки
58
- * Но в теории на них можно повешать все модальные и прочие действия
59
- *
60
- * !!!: в функции get() работает весь роутинг, в т.ч. вложенный для страниц
61
- *
62
- * See also:
63
- * - https://developer.mozilla.org/en-US/docs/Web/API/History_API
64
- */
65
- basePage: ()=>{},
66
- defTitle: '',
67
- actions: {},
68
- commands: {},
83
+ // deprecated!!
84
+ /** @import { NewHelper } from './newHelper.d.ts' */
85
+ Intl.newHelper=function(_='_') {
86
+ if (!window[_])
87
+ window[_] = {};
88
+
89
+ window[_].ver = '2.1.8';
90
+
91
+ window[_].link = {
92
+ /*
93
+ * МОДУЛЬ ССЫЛОК
94
+ * Author: MIOBOMB (2023-2026)
95
+ * Last patch: 2.1.7
96
+ *
97
+ * Работает по принципу [ссылка, команды...]
98
+ * Пример: ?home&debug&lang=ru
99
+ * ^^^^ ^^^^^^^^^^^^^
100
+ * страница команды
101
+ *
102
+ * В процессе разработки ядра 2.0 в Object hub я понял
103
+ * Что команды могут быть очень полезными для отладки
104
+ * Но в теории на них можно повешать все модальные и прочие действия
105
+ *
106
+ * !!!: в функции get() работает весь роутинг, в т.ч. вложенный для страниц
107
+ * !!!: пожалуйста, относитесь к этому модулю не как к "компонентной модели"
108
+ * или что у вас там в реактивном вебе.
109
+ * А относитесь к нему как к серверному маршрутизатору,
110
+ * вы же ведь можете сделать красивые и адекватные маршруты.
111
+ *
112
+ * See also:
113
+ * - https://developer.mozilla.org/en-US/docs/Web/API/History_API
114
+ */
115
+ basePage: ()=>{},
116
+ defTitle: '',
117
+ actions: {},
118
+ commands: {},
119
+
120
+ _i: true, // _i - блокировщик pushState в set()
121
+ _pop() {
122
+ /*
123
+ * Popstate движок
124
+ *
125
+ * Сделан он для сохранения команд в истории
126
+ * Это уникальная фича newHelper.js
127
+ *
128
+ * Зачем я сделал сохранение команд?
129
+ * Он вырос из потребностей object hub
130
+ * Для меня это потребность сохранять
131
+ * отладочные состояния в url
132
+ * или вызывать функции одноразки, например:
133
+ * - вызов модалки с конкретными данными (&user=1)
134
+ * - смена настроек SPA по команде (язык, тема...)
135
+ * -
136
+ * - сброс localStorage
137
+ * для вас это может быть всё что угодно
138
+ * но если он вам не нужен - link.popInit=true
139
+ *
140
+ * popstate срабатывает когда:
141
+ * - пользователь прыгает по истории назад/вперёд
142
+ * - мы вызываем history.pushState (не replaceState)
143
+ *
144
+ * _i различает эти случаи:
145
+ * true = пользователь прыгнул назад
146
+ * false = страница пишет свой адрес в ссылку
147
+ */
148
+ // HMM: некоторые браузеры могут вызывать popstate и при реплейсе
149
+ if (!window[_].link._i) {
150
+ // здесь происходит перенос команд при popstate
151
+ // читайте window[_].link.get() если хотите узнать почему
152
+ let newUrl='?' + [window[_].link.compile()[0],...window[_].link._cmd].join('&');
153
+ window[_].link._i=true;
154
+ history.replaceState(null,null,newUrl);
155
+ window[_].link.get();
156
+ } else
157
+ window[_].link._i=false;
158
+ },
159
+ _cmd: [],
160
+ popInit: false,
161
+ _init() {
162
+ if (!window[_].link.popInit) {
163
+ window.addEventListener('popstate', ()=>window[_].link._pop());
164
+ window[_].link.popInit = true;
165
+ }
166
+ },
167
+
168
+ compile: (e=location.search)=>e.replace('?','').split('&'),
169
+ set(page, title = window[_].link.defTitle) {
170
+ if (title) document.title = title;
171
+ if (!window[_].link._i) {
172
+ let link = window[_].link.compile();
173
+ link[0] = page;
174
+ history.pushState(null,null,'?'+link.join('&'));
175
+ }
176
+ window[_].link._i = false;
177
+ },
178
+ add(cmd) {
179
+ let link = window[_].link.compile();
180
+ if (!link.includes(cmd)) {
181
+ link.push(cmd);
182
+ window[_].link._cmd.push(cmd);
183
+ history.replaceState(null,null,'?'+link.join('&'));
184
+ }
185
+ },
186
+ remove(cmd) {
187
+ let link = window[_].link.compile();
188
+ if (link.includes(cmd)){
189
+ let c = window[_].link._cmd;
190
+ link.splice(link.indexOf(cmd),1);
191
+ c.splice(c.indexOf(cmd),1);
192
+ history.replaceState(null,null,'?'+link.join('&'));
193
+ }
194
+ },
195
+
196
+ get() {
197
+ window[_].link._init();
198
+ /*
199
+ * Страницы бросают ошибку чтобы вызвать базовую страницу
200
+ * Команды тем временем так не делают
201
+ * Потому что сломанная команда не так страшна как сломанная страница
202
+ * И вдруг на вашем сайте висит трекер от гугла который что то пишет в url
203
+ *
204
+ * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
205
+ * Сделано это для переноса команд при прыжках по истории
206
+ */
207
+ let links = window[_].link.compile(),
208
+ [ firstKey, fisrtValue ] = links[0].split('='),
209
+ cmds = links.slice(1);
210
+ try {
211
+ let route = firstKey.split('/'),
212
+ dir = window[_].link.actions,
213
+ main = dir[firstKey];
214
+ if (!firstKey.includes('/')) {
215
+ main(fisrtValue);
216
+ } else {
217
+ /*
218
+ * ВЛОЖЕННЫЙ РОУТЕР
219
+ *
220
+ * Фича которую я сделал случайно
221
+ * в поединке с бекендером-вайбкодером
222
+ *
223
+ * Заодно со скуки я сделал динамические маршруты
224
+ * Я офигел когда понял что они полностью рабочие
225
+ * Впрочем динамика работает также как и везде
226
+ * называете свой ключ с двоеточия и всё работает
227
+ *
228
+ * !!!:
229
+ * Чтобы создать вложенность вам нужно
230
+ * сделать объект вместо функции
231
+ * и обязательно добавить "/" в конце ключа
232
+ * и внутри объекта уже описывать либо
233
+ * ещё большую вложенность, либо маршруты
234
+ * ТАКЖЕ
235
+ * Если вы используете динамический роутер
236
+ * Ваши query параметры будут удалены
237
+ * А сама переменная передаваемая в функцию
238
+ * Станет массивом, который нужно раскрыть
239
+ * Количество динамики в пути - количество элементов массива
240
+ *
241
+ * Пример роутера:
242
+ *
243
+ * {
244
+ * '': ()=>mainPage(),
245
+ * 'account/': {
246
+ * '': ()=>profile(),
247
+ * 'settings':()=>settings()
248
+ * },
249
+ * 'product': e=>getProduct(e),
250
+ * 'user/': {
251
+ * ':id': ...e=>getProfile(...e)
252
+ * }
253
+ * }
254
+ *
255
+ * FIXME:
256
+ * сделать документацию
257
+ * или хотябы интродакшн с интерактивом
258
+ */
259
+ let dynamic = [];
260
+ for (let point of route){
261
+ let isDyn = Object.keys(dir).find(e=>e.startsWith(':'));
262
+ if (isDyn) {
263
+ dynamic.push(point);
264
+ point = isDyn;
265
+ if (point.endsWith('/'))
266
+ point = point.slice(0,-1);
267
+ }
268
+ let kDir = dir[point+'/'];
269
+ if (kDir)
270
+ dir = kDir;
271
+ else {
272
+ if (dynamic.length)
273
+ fisrtValue = dynamic;
274
+ dir[point](fisrtValue);
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ } catch (e) {
280
+ window[_].link.basePage();
281
+ throw e;
282
+ }
283
+ window[_].link._cmd = cmds;
284
+ cmds.forEach(cmdPre => {
285
+ let [ key, value ] = cmdPre.split('=');
286
+ let cmd = window[_].link.commands[key];
287
+ if (cmd)
288
+ cmd(value);
289
+ else
290
+ console.error(new Error(`command '${cmd}' doesn't exist!`))
291
+ });
292
+ },
293
+ };
294
+
295
+ window[_].lazy = {
296
+ /*
297
+ * МОДУЛЬ ЛЕНИ
298
+ * Author: MIOBOMB (2024-2026)
299
+ * Last patch: 2.1.4
300
+ *
301
+ * Создаёт в глобальной области видимости прокси функции
302
+ * Вызывающие загрузку скрипта с внещним модулем
303
+ * Был сделан через глобальную область, так намного проще создавать лень
304
+ *
305
+ * !!!: Функции обёртки в register() должны быть повешаны на window
306
+ * Иначе lazy._ провалится в рекурсию ошибок, не наступайте на мои грабли
307
+ *
308
+ * HMM: будет ли легче создавать лень в легаси проектах через es6 импорты
309
+ *
310
+ * See also:
311
+ * - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
312
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Window/window
313
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function (для _())
314
+ */
315
+ loaded: {},
316
+ load(url, ...args) {
317
+ /*
318
+ * ...args передаются в Promise.resolve(args)
319
+ * Это позволяет делать window[_].lazy.load('script.js', 'данные', 'для', 'колбека')
320
+ * И потом в .then((a,b,c)=>...) получать эти аргументы
321
+ *
322
+ * Тройное состояние скрипта в lazy.loaded:
323
+ * - true: уже загружен => сразу резолвим
324
+ * - Promise: грузится сейчас => ждём тот же промис
325
+ * - undefined: ещё не грузили => создаём новый промис
326
+ *
327
+ * Это защита от двойной загрузки одного скрипта
328
+ */
329
+ let key = url.split('?')[0], // отсекаем параметры, чтобы не дублировать
330
+ state = window[_].lazy.loaded;
331
+ if (state[key] === true)
332
+ return Promise.resolve(args);
333
+ if (state[key] instanceof Promise)
334
+ return state[key].then(()=>args);
335
+
336
+ let promise = new Promise((resolve,reject)=>{
337
+ let scr = document.createElement('script');
338
+ scr.src = url;
339
+ scr.onload = ()=>{
340
+ state[key] = true;
341
+ resolve(args);
342
+ };
343
+ scr.onerror = ()=>{
344
+ delete state[key];
345
+ reject(new Error('Failed to load '+url));
346
+ };
347
+ document.head.append(scr);
348
+ });
349
+ state[key] = promise;
350
+ return promise;
351
+ },
352
+ register(script, funcs) {
353
+ for (let fn of funcs) {
354
+ let fns = fn.split('.'),
355
+ method = fns.pop(),
356
+ path = window;
357
+ for (let obj of fns) {
358
+ if (path[obj] == undefined)
359
+ path[obj] = {};
360
+ path = path[obj];
361
+ }
362
+ path[method] = (...a)=>
363
+ window[_].lazy._(script,fn).then(f=>f(...a));
364
+ }
365
+ },
366
+ async _(scr, fn) {
367
+ let get = path => path.split('.').reduce((obj, key) => obj?.[key], window),
368
+ wrapper = get(fn);
369
+
370
+ await window[_].lazy.load(scr); // await короче Promise.then
371
+
372
+ if (wrapper !== get(fn))
373
+ return get(fn);
374
+ throw new Error(`Function ${fn} not loaded from ${scr}`);
375
+ },
376
+ };
377
+
378
+ window[_].lang = {
379
+ /*
380
+ * МОДУЛЬ ПЕРЕВОДОВ (l10n)
381
+ * Author: MIOBOMB (2024-2026)
382
+ * Contributors:
383
+ * - DenisC - логика метода load + патч всего модуля (2025)
384
+ * Last patch: 2.1.7
385
+ *
386
+ * По слухам этот модуль лучше чем многие i18n реализации, и лучше всех l10n
387
+ * Всё потому что он из коробки умеет переводить страницу без перезагрузки
388
+ *
389
+ * !!!: parse() обрабатывает ключи из vars и подставляет их значения
390
+ * ваш +ключ+ становится значением, и это значение динамичное
391
+ * Так удобнее отображать динамичные данные на сайтах
392
+ * Например никнейм пользователя
393
+ *
394
+ * !!!: Это l10n (локализация), а не i18n (интернационализация)
395
+ *
396
+ * i18n — подготовка кода: вынос строк в JSON, поддержка Unicode,
397
+ * гибкая верстка. Делается один раз.
398
+ *
399
+ * l10n - перевод JSON и адаптация под язык/регион
400
+ *
401
+ * lang - загружает JSON, подставляет +переменные+,
402
+ * даёт реактивную смену языка на странице
403
+ *
404
+ * Чтобы частично приблизить lang к i18n используйте Intl,
405
+ * Нативное апи интернационализации (даты, числа, валюты)
406
+ *
407
+ * See also:
408
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
409
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
410
+ * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset (data-trans атрибуты)
411
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
412
+ * - https://localizejs.com/articles/i18n-vs-l10n
413
+ */
414
+ addr: '',
415
+ vars: {},
416
+ // HMM:
417
+ // переделать main на мапу т.к. внутреннее api?
418
+ // или сохранить оригинальное api на объекте
419
+ main: {},
420
+
421
+ load: name => fetch(window[_].lang.addr + name + '.json')
422
+ .then(r => r.text()),
423
+ parse: (packet, vars = window[_].lang.vars)=>
424
+ // HMM: переделать под общий синтаксис типа {var}
425
+ packet.replace(/\+([^+]+)\+/g, (match, key)=>{
426
+ let v = vars[key];
427
+ return v !== undefined ? v : match;
428
+ }),
429
+ async replace(name){
430
+ const packet = await window[_].lang.load(name);
431
+ window[_].lang.main = JSON.parse(window[_].lang.parse(packet)); // без замены языка нельзя начинать перевод
432
+
433
+ for (let el of document.querySelectorAll(`[${window[_].lang.attr}]`)) {
434
+ let key = el.dataset.trans,
435
+ text = window[_].lang.main[key] || key,
436
+ tag = el.tagName;
437
+
438
+ if (tag === 'IMG')
439
+ el.src = text;
440
+ else if (['INPUT','TEXTAREA'].includes(tag))
441
+ el[ el.type === 'submit' ? 'value' : 'placeholder' ] = text;
442
+ else
443
+ el.innerHTML = text;
444
+ }
445
+ // возвращаем для последующей обработки пакета, например для сохранения в window[_].storage
446
+ return packet;
447
+ },
448
+
449
+ /*
450
+ * Получатели строки из пакета автоматически формируют HTML
451
+ * Это позволяет заметно упростить работу с кодом
452
+ * Вместо отдельного указания data-trans и lang.from
453
+ * вы можете написать `<h1${window[_].lang.text('yourKey')}/h1>`
454
+ * А пришлось бы писать `<h1 data-trans="yourKey">${window[_].lang.from('yourKey')}</h1>`
455
+ * Согласитесь, и короче и удобнее ведь?
456
+ * Не повторяйте моих ошибок и примите это как победу в лотерее
457
+ *
458
+ * !!!: если ключа в пакете нету, будет выброшен warning
459
+ */
460
+ attr: ` data-trans`,
461
+ from: i=>window[_].lang.main[i] || console.warn(`window[_].lang> ${i} is undefined`) || i,
462
+
463
+ text: i=>window[_].lang.attr+`="${i}">${window[_].lang.from(i)}<`,
464
+ submit: i=>window[_].lang.attr+`="${i}" value="${window[_].lang.from(i)}">`, // <input type=submit>
465
+ input: i=>window[_].lang.attr+`="${i}" placeholder="${window[_].lang.from(i)}">`,
466
+ textarea: i=>window[_].lang.attr+`="${i}" placeholder="${window[_].lang.from(i)}"><`,
467
+ img: i=>window[_].lang.attr+`="${i}" src="${window[_].lang.from(i)}"`,
468
+ winTitle(i) {
469
+ let text = window[_].lang.from(i),
470
+ dataTrans = window[_].lang.attr+`="${i}"`;
471
+ if (text == null || text == '') {
472
+ text = i;
473
+ dataTrans = '';
474
+ }
475
+ return `${dataTrans}>${text}<`;
476
+ },
477
+ };
478
+
479
+ window[_].http = {
480
+ /*
481
+ * HTTP-КЛИЕНТ
482
+ * Author: MIOBOMB (2024-2026)
483
+ * Last patch: 2.1.4
484
+ *
485
+ * Обычная обёртка нав XHR для быстрых запросов
486
+ * Использую XHR вместо fetch
487
+ * Мне нужен прогресс загрузки (fetch его не даёт)
488
+ * Да и вам тоже не помешает прогресс загрузки
489
+ *
490
+ * В defaultHeaders вы можете установить хедеры по умолчанию
491
+ * Как пример Authorization: 'your token'
492
+ * HMM: добавить возможность игнорировать дефолтные хедеры
493
+ *
494
+ * See also:
495
+ * - https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
496
+ * - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/progress
497
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
498
+ */
499
+ defaultHeaders: {},
500
+ req(method, url, data = '', headers = {}, fileProgressElement = false) {
501
+ return new Promise((resolve, reject)=>{
502
+ let xhr = new XMLHttpRequest();
503
+
504
+ xhr.open(method, url);
505
+
506
+ let allHeaders = { ...window[_].http.defaultHeaders, ...headers };
507
+ for (let header in allHeaders)
508
+ xhr.setRequestHeader(header, allHeaders[header]);
509
+
510
+ // !!!: fileProgressElement ожидает <progress> элемент без min/max
511
+ // Потому что value от 0 до 1
512
+ if (fileProgressElement)
513
+ xhr.upload.onprogress= e=>{
514
+ if (e.lengthComputable) {
515
+ let percentage = (e.loaded / e.total);
516
+ fileProgressElement.setAttribute('value', percentage);
517
+ }
518
+ };
519
+
520
+ xhr.onreadystatechange= ()=>{
521
+ if (xhr.readyState=== 4)
522
+ if (xhr.status >= 200 && xhr.status < 300)
523
+ resolve(xhr.response);
524
+ else
525
+ reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
526
+ };
527
+ xhr.onerror = ()=>
528
+ reject(new Error('Network error'), xhr);
529
+
530
+ xhr.send(data);
531
+ });
532
+ },
533
+ get: (url, headers={})=>
534
+ window[_].http.req('GET', url, false, headers),
535
+ post: (url, data = '', headers = {}, fileProgressElement = false)=>
536
+ window[_].http.req('POST', url, data, headers, fileProgressElement)
537
+ };
538
+
539
+ window[_].html = function(strs, ...args) {
540
+ /*
541
+ * Шаблонные строки в DOM
542
+ * Author: MIOBOMB (2026)
543
+ * Last patch: 2.1.4
544
+ *
545
+ * Позволяет писать window[_].html`<div>${content}</div>`
546
+ * И получать настоящий DOM-элемент, а не строку
547
+ *
548
+ * Почему через template?
549
+ * - Скрипты не выполняются (никаких xss!)
550
+ * - Можно создать несколько элементов разом
551
+ * - Быстрее чем createElement для сложных структур
552
+ * - Банально удобнее createElement для сложных древ
553
+ *
554
+ * HMM: проверить производительность этого генератора dom
555
+ * HMM: проверить факт о "защите от xss" из-за on* атрибутов
556
+ *
557
+ * See also:
558
+ * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement
559
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
560
+ */
561
+ let fullStr = '',
562
+ DOMs = [];
563
+ for (let i=0; i < args.length; i++) {
564
+ fullStr += strs[i];
565
+ let arg = args[i];
566
+ if (arg && arg.nodeType) {
567
+ fullStr += `<!--${DOMs.length}-->`;
568
+ DOMs.push(arg);
569
+ } else {
570
+ fullStr += arg;
571
+ }
572
+ }
573
+ fullStr += strs[strs.length - 1];
574
+
575
+ const template = document.createElement('template');
576
+ template.innerHTML = fullStr;
577
+ const content = template.content;
578
+
579
+ // для создания вложенности html элементов заменяем плейсхолдеры
580
+ const it = document.createTreeWalker(
581
+ content,
582
+ NodeFilter.SHOW_COMMENT
583
+ );
584
+ let node, i = 0;
585
+ for (; node = it.nextNode(); )
586
+ node.replaceWith(DOMs[i++]);
587
+
588
+ if (content.children.length === 1)
589
+ return content.firstChild;
590
+ return content;
591
+ };
592
+
593
+ window[_].pipe = function(data, ...fns) {
594
+ /*
595
+ * КАСТОМНЫЙ PIPE ОПЕРАТОР
596
+ * Author: MIOBOMB (2026)
597
+ * Last patch: 2.1.4
598
+ *
599
+ * Никакой магии, обычный синхронный |>
600
+ * для мутации таблиц будет самое то
601
+ *
602
+ * See also:
603
+ * - https://github.com/tc39/proposal-pipeline-operator/blob/main/README.md
604
+ */
605
+ for (const fn of fns)
606
+ data = fn(data);
607
+ return data;
608
+ };
609
+
610
+ window[_].pipeAsync = async function(data, ...fns) {
611
+ /*
612
+ * КАСТОМНЫЙ PIPE ОПЕРАТОР 2
613
+ * Author: MIOBOMB (2026)
614
+ * Last patch: 2.1.7
615
+ *
616
+ * Никакой магии, обычный асинхронный |>
617
+ * для получения и мутации данных сойдёт
618
+ *
619
+ * See also:
620
+ * - https://github.com/tc39/proposal-pipeline-operator/blob/main/README.md
621
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
622
+ */
623
+ for (const fn of fns) {
624
+ let waiter = await data;
625
+ data = await fn(waiter);
626
+ }
627
+ return data;
628
+ };
629
+
630
+ window[_].form = { // TS DONE HMM no
631
+ /*
632
+ * АВТОСОХРАНЕНИЕ ФОРМ
633
+ * Author: MIOBOMB (2026)
634
+ * Last patch: 2.1.4
635
+ *
636
+ * Позволяет сохранять состояние формы на случай
637
+ * Если в офисе внезапно выключат свет
638
+ *
639
+ * HMM: может сделать более полноценный модуль форм
640
+ * с встроенной валидацией, или чем нибуть ещё
641
+ *
642
+ * See also:
643
+ * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
644
+ * - https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
645
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
646
+ */
647
+ read(form) {
648
+ let data = {};
649
+ new FormData(form).forEach((value, key)=>{
650
+ if (data[key] !== undefined) {
651
+ if (!Array.isArray(data[key]))
652
+ data[key] = [data[key]];
653
+ else
654
+ data[key].push(value);
655
+ } else
656
+ data[key] = value;
657
+ });
658
+ return data;
659
+ },
660
+ write(form, data) {
661
+ Object.entries(data).forEach(([key,value])=>{
662
+ let el = form.elements[key];
663
+ if (!el)
664
+ return;
665
+ if (el.length)
666
+ [...el].forEach((opt,i)=>{
667
+ let isCheckBox = 'selected';
668
+ if (['checkbox','radio'].includes(opt.type))
669
+ isCheckBox = 'checked';
670
+
671
+ let select = false;
672
+ if (Array.isArray(value)) {
673
+ if (value.includes(opt.value))
674
+ select = true;
675
+ } else if (opt.value == value)
676
+ select = true;
677
+
678
+ opt[isCheckBox] = select;
679
+ });
680
+ else
681
+ el.value = value;
682
+ });
683
+ return data;
684
+ },
685
+ };
686
+
687
+ window[_].storage = class {
688
+ /*
689
+ * ИЗОЛЯТОР ХРАНИЛИЩ
690
+ * Author: MIOBOMB (2024-2026)
691
+ * Last patch: 2.1.0
692
+ *
693
+ * Обычная обёртка поверх Storage экземпляра
694
+ * Даёт простую но надёжную изоляцию хранилищ
695
+ * Но нет она не даёт вам защиту от угона хранилища
696
+ *
697
+ * See also:
698
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
699
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
700
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Storage
701
+ */
702
+ constructor(storage, name) {
703
+ this.s = storage;
704
+ this.n = name;
705
+ }
706
+ get = key=>
707
+ this.s.getItem(this.n + key);
708
+ set = (key, value)=>
709
+ this.s.setItem(this.n + key, value);
710
+ remove = key=>
711
+ this.s.removeItem(this.n + key);
712
+ clear = ()=>Object.keys(this.s)
713
+ .filter(k => k.startsWith(this.n))
714
+ .forEach(k => this.s.removeItem(k));
715
+ };
716
+
717
+ window[_].err = {
718
+ /*
719
+ * МОДУЛЬ ОШИБОК
720
+ * Author: MIOBOMB (2024-2026)
721
+ * Last patch: 2.1.4
722
+ *
723
+ * Мой самописный модуль ошибок
724
+ * вообще он ялвяется наследием
725
+ * но если вам лень писать .catch после .then
726
+ * то почему бы и нет
727
+ *
728
+ * See also:
729
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
730
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
731
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
732
+ */
733
+ init() {
734
+ window.addEventListener('error',window[_].err.handleGlobal);
735
+ window.addEventListener('unhandledrejection',window[_].err.handleRejection);
736
+ },
737
+ print: (cnt,e)=>console.error(e),
738
+
739
+ errors: {},
740
+ _c: 0,
741
+ log(err) {
742
+ window[_].err.print(window[_].err._c,err);
743
+ window[_].err._c++;
744
+ window[_].err.errors[window[_].err._c]=err;
745
+ },
746
+ handleGlobal(message,source,line,column,error){
747
+ console.error(message,source+':'+line+':'+column,error)
748
+ window[_].err.log(message + `\n IN ${source} ON LINE ${line} IN COLUMN ${column}`);
749
+ },
750
+ handleRejection(e){
751
+ const err = e.reason || e;
752
+ console.error(err);
753
+ window[_].err.log(
754
+ `PROMISE ERROR\n`+
755
+ `${e.stack || e}`
756
+ );
757
+ },
758
+ };
69
759
 
70
- _i: true, // _i - блокировщик pushState в set()
71
- _pop() {
72
- /*
73
- * Popstate движок
74
- *
75
- * Сделан он для сохранения команд в истории
76
- * Это уникальная фича newHelper.js
77
- *
78
- * popstate срабатывает когда:
79
- * - пользователь прыгает по истории назад/вперёд
80
- * - мы вызываем history.pushState (не replaceState)
81
- *
82
- * _i различает эти случаи:
83
- * true = пользователь прыгнул назад
84
- * false = страница пишет свой адрес в ссылку
85
- */
86
- // ???: некоторые браузеры могут вызывать popstate и при реплейсе
87
- if (!this._i) {
88
- // здесь происходит перенос команд при popstate
89
- // читайте _.link.get() если хотите узнать почему
90
- let newUrl='?' + [this.compile()[0],...this._cmd].join('&');
91
- this._i=true;
92
- history.replaceState(null,null,newUrl);
93
- this.get();
94
- } else
95
- this._i=false;
96
- },
97
- _cmd: [],
98
- _popInit: false,
99
- _init() {
100
- if (!this._popInit) {
101
- window.addEventListener('popstate', ()=>this._pop());
102
- this._popInit = true;
103
- }
104
- },
760
+ window[_].hotkeys = {
761
+ /*
762
+ * ГОРЯЧИЕ КЛАВИШЫ
763
+ * Author: MIOBOMB (2025-2026)
764
+ * Last patch: 2.1.4
765
+ *
766
+ * Реализует самый настоящий press/release интерфейс
767
+ * Если верить минификатору, после сжатия весит всего 790 байт
768
+ *
769
+ * В Object Hub уже есть текстовый редактор горячих клавиш
770
+ * На базе этого движка, конечно давать textarea с js кодом...
771
+ * Не самая безопасная затея, но как факт кастомизация широчайшая
772
+ *
773
+ * _holds работает не на массивах а на new Set()
774
+ * Сеты работают намного быстрее при большом объёме данных
775
+ * Вы же не хотите чтобы у вас тормозил поток с 100+ хоткеями
776
+ * Из-за простого печатанья?
777
+ *
778
+ * FIXME: Рассмотреть альтернативы e.code из-за проблем
779
+ * с otg-клавиатурами на телефонах
780
+ *
781
+ * See also:
782
+ * - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
783
+ * - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
784
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
785
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event
786
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
787
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
788
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
789
+ * - https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values (список клавиш)
790
+ */
791
+ keys: new Map(),
792
+ _holds: new Set(),
793
+ _: false,
794
+
795
+ _parse: combo => combo.split('+').map(k=>k.trim()),
796
+ _match(keys) {
797
+ // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
798
+ for (let k of keys) if (!window[_].hotkeys._holds.has(k)) return false;
799
+ return true;
800
+ },
801
+ _init() {
802
+ if (window[_].hotkeys._)
803
+ return;
804
+ document.addEventListener('keydown', e=>{
805
+ window[_].hotkeys._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
806
+ // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
807
+
808
+ for (let hotkey of window[_].hotkeys.keys.values()) {
809
+ if (!window[_].hotkeys._match(hotkey.keys))
810
+ continue;
811
+ if (hotkey.press && !hotkey.active) {
812
+ hotkey.active = true; // active защищает от множественных срабатываний
813
+ hotkey.press(e);
814
+ }
815
+ }
816
+ });
817
+ document.addEventListener('keyup', e=>{
818
+ window[_].hotkeys._holds.delete(e.code);
819
+
820
+ for (let hotkey of window[_].hotkeys.keys.values()) {
821
+ if (hotkey.active && !window[_].hotkeys._match(hotkey.keys)) {
822
+ hotkey.active=false;
823
+ hotkey.release(e);
824
+ }
825
+ }
826
+ });
827
+ window.addEventListener('blur', e=>{
828
+ /*
829
+ * При переключении в другое окно автоматического keyup не будет
830
+ * Поэтому сбрасываем всё принудительно, мало ли
831
+ */
832
+ for (let hotkey of window[_].hotkeys.keys.values()) {
833
+ if (hotkey.active) {
834
+ hotkey.active = false;
835
+ hotkey.release();
836
+ }
837
+ }
838
+ window[_].hotkeys._holds.clear();
839
+ });
840
+ window[_].hotkeys._=true;
841
+ },
842
+ on(combo, press, release) {
843
+ window[_].hotkeys._init();
844
+ let keys = window[_].hotkeys._parse(combo);
845
+
846
+ window[_].hotkeys.keys.set(combo, {
847
+ keys,
848
+ // press/releace по умолчанию пустышки для сокращения синаксиса
849
+ press: press || (()=>{}),
850
+ release: release || (()=>{}),
851
+ active: false
852
+ });
853
+
854
+ return this;
855
+ },
856
+ off(combo) {
857
+ window[_].hotkeys.keys.delete(combo);
858
+ return this;
859
+ },
860
+ };
105
861
 
106
- compile: ()=>location.search.replace('?','').split('&'),
107
- set(page, title = this.defTitle) {
108
- if (title) document.title = title;
109
- if (!this._i) {
110
- let link = this.compile();
111
- link[0] = page;
112
- history.pushState(null,null,'?'+link.join('&'));
113
- }
114
- this._i = false;
115
- },
116
- add(cmd) {
117
- let link = this.compile();
118
- if (!link.includes(cmd)) {
119
- link.push(cmd);
120
- this._cmd.push(cmd);
121
- history.replaceState(null,null,'?'+link.join('&'));
122
- }
123
- },
124
- remove(cmd) {
125
- let link = this.compile();
126
- if (link.includes(cmd)){
127
- let c = this._cmd;
128
- link.splice(link.indexOf(cmd),1);
129
- c.splice(c.indexOf(cmd),1);
130
- history.replaceState(null,null,'?'+link.join('&'));
131
- }
132
- },
133
- get() {
134
- this._init();
135
- /*
136
- * Страницы бросают ошибку чтобы вызвать базовую страницу
137
- * Команды тем временем так не делают
138
- * Потому что сломанная команда не так страшна как сломанная страница
139
- *
140
- * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
141
- * Сделано это для переноса команд при прыжках по истории
142
- */
143
- let links = this.compile(),
144
- [ firstKey, fisrtValue ] = links[0].split('='),
145
- cmds = links.slice(1);
146
- try {
147
- let dirs = firstKey.split('/'),
148
- dir = this.actions,
149
- main = dir[firstKey];
150
- if (!firstKey.includes('/')) {
151
- main(fisrtValue);
152
- } else {
153
- for (let p of dirs){
154
- let kDir = dir[p+'/'];
155
- if (kDir)
156
- dir = kDir;
157
- else{
158
- dir[p](fisrtValue);
159
- break;
160
- }
161
- }
162
- }
163
- } catch (e) {
164
- this.basePage();
165
- throw e;
166
- }
167
- this._cmd = cmds;
168
- cmds.forEach(cmdPre => {
169
- let [ key, value ] = cmdPre.split('=');
170
- let cmd = this.commands[key];
171
- if (cmd)
172
- cmd(value);
173
- else
174
- console.error(new Error(`command '${cmd}' doesn't exist!`))
175
- });
176
- },
177
- },
178
- lazy: {
179
- /*
180
- * МОДУЛЬ ЛЕНИ
181
- *
182
- * Создаёт в глобальной области видимости прокси функции
183
- * Вызывающие загрузку скрипта с внещним модулем
184
- * Был сделан через глобальную область, так намного проще создавать лень
185
- *
186
- * !!!: Функции обёртки в register() должны быть повешаны на window
187
- * Иначе lazy._ провалится в рекурсию ошибок, не наступайте на мои грабли
188
- *
189
- * ???: будет ли легче создавать лень в легаси проектах через es6 импорты
190
- *
191
- * See also:
192
- * - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
193
- * - https://developer.mozilla.org/en-US/docs/Web/API/Window/window
194
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function (для lazy())
195
- */
196
- loaded: {},
197
- load(url, ...args) {
198
- /*
199
- * ...args передаются в Promise.resolve(args)
200
- * Это позволяет делать _.lazy.load('script.js', 'данные', 'для', 'колбека')
201
- * И потом в .then((a,b,c)=>...) получать эти аргументы
202
- *
203
- * Тройное состояние скрипта в lazy.loaded:
204
- * - true: уже загружен => сразу резолвим
205
- * - Promise: грузится сейчас => ждём тот же промис
206
- * - undefined: ещё не грузили => создаём новый промис
207
- *
208
- * Это защита от двойной загрузки одного скрипта
209
- */
210
- let key = url.split('?')[0], // отсекаем параметры, чтобы не дублировать
211
- state = this.loaded;
212
- if (state[key] === true)
213
- return Promise.resolve(args);
214
- if (state[key] instanceof Promise)
215
- return state[key].then(()=>args);
216
-
217
- let promise = new Promise((resolve,reject)=>{
218
- let scr = document.createElement('script');
219
- scr.src = url;
220
- scr.onload = ()=>{
221
- state[key] = true;
222
- resolve(args);
223
- };
224
- scr.onerror = ()=>{
225
- delete state[key];
226
- reject(new Error('Failed to load '+url));
227
- };
228
- document.head.append(scr);
229
- });
230
- state[key] = promise;
231
- return promise;
232
- },
233
- register(script, funcs) {
234
- if (!Array.isArray(funcs))
235
- return new Error('Array required for register');
236
-
237
- for (let fn of funcs) {
238
- let fns = fn.split('.'),
239
- method = fns.pop(),
240
- path = window;
241
- for (let obj of fns) {
242
- if (path[obj] == undefined)
243
- path[obj] = {};
244
- path = path[obj];
245
- }
246
- path[method] = (...a)=>
247
- this.lazy(script,fn).then(f=>f(...a));
248
- }
249
- },
250
- async lazy(scr, fn) {
251
- let get = path => path.split('.').reduce((obj, key) => obj?.[key], window),
252
- wrapper = get(fn);
253
-
254
- await this.load(scr); // await короче Promise.then
255
-
256
- if (wrapper !== get(fn))
257
- return get(fn);
258
- throw new Error(`Function ${fn} not loaded from ${scr}`);
259
- },
260
- },
261
- lang: {
262
- /*
263
- * МОДУЛЬ ПЕРЕВОДОВ (l10n)
264
- *
265
- * По слухам этот модуль лучше чем многие i18n реализации, и лучше всех l10n
266
- * Всё потому что он из коробки умеет переводить страницу без перезагрузки
267
- *
268
- * !!!: parse() обрабатывает ключи из vars и подставляет их значения
269
- * ваш +ключ+ становится значением, и это значение динамичное
270
- * Так удобнее отображать динамичные данные на сайтах
271
- * Например никнейм пользователя
272
- *
273
- * !!!: Это l10n (локализация), а не i18n (интернационализация)
274
- *
275
- * i18n — подготовка кода: вынос строк в JSON, поддержка Unicode,
276
- * гибкая верстка. Делается один раз.
277
- *
278
- * l10n - перевод JSON и адаптация под язык/регион
279
- *
280
- * lang - загружает JSON, подставляет +переменные+,
281
- * даёт реактивную смену языка на странице
282
- *
283
- * Чтобы частично приблизить lang к i18n используйте Intl,
284
- * Нативное апи интернационализации (даты, числа, валюты)
285
- *
286
- * See also:
287
- * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
288
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
289
- * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset (data-trans атрибуты)
290
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
291
- * - https://localizejs.com/articles/i18n-vs-l10n
292
- */
293
- addr: '',
294
- vars: {},
295
- // FIXME: переделать main на мапу т.к. внутреннее api?
296
- // или сохранить оригинальное api на объекте
297
- main: {},
298
-
299
- load: name => _.http.req('GET', _.lang.addr + name + '.json'),
300
- parse: (packet, vars = _.lang.vars)=>
301
- // ???: переделать под общий синтаксис типа {var}
302
- packet.replace(/\+([^+]+)\+/g, (match, key)=>{
303
- let v = vars[key];
304
- return v !== undefined ? v : match;
305
- }),
306
- async replace(name){
307
- const packet = await this.load(name);
308
- this.main = JSON.parse(this.parse(packet)); // без замены языка нельзя начинать перевод
309
-
310
- for (let el of document.querySelectorAll('[data-trans]')) {
311
- let key = el.dataset.trans,
312
- text = this.main[key] || key,
313
- tag = el.tagName;
314
-
315
- if (tag === 'IMG')
316
- el.src = text;
317
- else if (['INPUT','TEXTAREA'].includes(tag))
318
- el[ el.type === 'submit' ? 'value' : 'placeholder' ] = text;
319
- else
320
- el.innerHTML = text;
321
- }
322
- // возвращаем для последующей обработки пакета, например для сохранения в _.storage
323
- return packet;
324
- },
325
-
326
- /*
327
- * Получатели строки из пакета автоматически формируют HTML
328
- * Это позволяет заметно упростить работу с кодом
329
- * Вместо отдельного указания data-trans и lang.from
330
- * вы можете написать `<h1${_.lang.text('yourKey')}/h1>`
331
- * А пришлось бы писать `<h1 data-trans="yourKey">${_.lang.from('yourKey')}</h1>`
332
- * Согласитесь, и короче и удобнее ведь?
333
- * Не повторяйте моих ошибок и примите это как победу в лотерее
334
- *
335
- * !!!: если ключа в пакете нету, будет выброшен warning
336
- */
337
- attr: i=>` data-trans="${i}"`,
338
- from: i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
339
-
340
- text: i=>_.lang.attr(i)+`>${_.lang.from(i)}<`,
341
- submit: i=>_.lang.attr(i)+`value="${_.lang.from(i)}">`, // <input type=submit>
342
- input: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}">`,
343
- textarea: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}"><`,
344
- img: i=>_.lang.attr(i)+`src="${_.lang.from(i)}"`,
345
- winTitle(i) {
346
- let text = this.from(i),
347
- dataTrans = this.attr(i);
348
- if (text == null || text == '') {
349
- text = i;
350
- dataTrans = '';
351
- }
352
- return `${dataTrans}>${text}<`;
353
- },
354
- },
355
- http: {
356
- /*
357
- * HTTP-КЛИЕНТ
358
- *
359
- * Обычная обёртка нав XHR для быстрых запросов
360
- * Использую XHR вместо fetch
361
- * Мне нужен прогресс загрузки (fetch его не даёт)
362
- * Да и вам тоже не помешает прогресс загрузки
363
- *
364
- * В defaultHeaders вы можете установить хедеры по умолчанию
365
- * Как пример Authorization: 'your token'
366
- * ???: добавить возможность игнорировать дефолтные хедеры
367
- *
368
- * See also:
369
- * - https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
370
- * - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/progress
371
- * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
372
- */
373
- defaultHeaders: {},
374
- req(method, url, data = '', headers = {}, fileProgressElement = false) {
375
- return new Promise((resolve, reject)=>{
376
- let xhr = new XMLHttpRequest();
377
-
378
- xhr.open(method, url);
379
-
380
- let allHeaders = { ...this.defaultHeaders, ...headers };
381
- for (let header in allHeaders)
382
- xhr.setRequestHeader(header, allHeaders[header]);
383
-
384
- // !!!: fileProgressElement ожидает <progress> элемент без min/max
385
- // Потому что value от 0 до 1
386
- if (fileProgressElement)
387
- xhr.upload.onprogress= e=>{
388
- if (e.lengthComputable) {
389
- let percentage = (e.loaded / e.total);
390
- fileProgressElement.setAttribute('value', percentage);
391
- }
392
- };
393
-
394
- xhr.onreadystatechange= ()=>{
395
- if (xhr.readyState=== 4)
396
- if (xhr.status >= 200 && xhr.status < 300)
397
- resolve(xhr.response);
398
- else
399
- reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
400
- };
401
- xhr.onerror = ()=>
402
- reject(new Error('Network error'), xhr);
403
-
404
- xhr.send(data);
405
- });
406
- },
407
- get: (url, headers={})=>
408
- _.http.req('GET', url, false, headers),
409
- post: (url, data = '', headers = {}, fileProgressElement = false)=>
410
- _.http.req('POST', url, data, headers, fileProgressElement)
411
- },
412
- html(strs, ...args) {
413
- /*
414
- * Шаблонные строки в DOM
415
- *
416
- * Позволяет писать _.html`<div>${content}</div>`
417
- * И получать настоящий DOM-элемент, а не строку
418
- *
419
- * Почему через template?
420
- * - Скрипты не выполняются (никаких xss!)
421
- * - Можно создать несколько элементов разом
422
- * - Быстрее чем createElement для сложных структур
423
- * - Банально удобнее createElement для сложных древ
424
- *
425
- * See also:
426
- * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement
427
- * - https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
428
- */
429
- let fullStr = '',
430
- DOMs = [];
431
- for (let i=0; i < args.length; i++) {
432
- fullStr += strs[i];
433
- let arg = args[i];
434
- if (arg && arg.nodeType) {
435
- fullStr += `<!--${DOMs.length}-->`;
436
- DOMs.push(arg);
437
- } else {
438
- fullStr += arg;
439
- }
440
- }
441
- fullStr += strs[strs.length - 1];
442
-
443
- const template = document.createElement('template');
444
- template.innerHTML = fullStr;
445
- const content = template.content;
446
-
447
- // для создания вложенности html элементов заменяем плейсхолдеры <!--${DOMs.length}-->
448
- const it = document.createTreeWalker(
449
- content,
450
- NodeFilter.SHOW_COMMENT
451
- );
452
- let node, i = 0;
453
- for (; node = it.nextNode(); )
454
- node.replaceWith(DOMs[i++]);
455
-
456
- if (content.children.length === 1)
457
- return content.firstChild;
458
- return content;
459
- },
460
- pipe(data, ...fns) {
461
- /*
462
- * КАСТОМНЫЙ PIPE ОПЕРАТОР
463
- *
464
- * Никакой магии, обычный синхронный |>
465
- * для мутации таблиц будет самое то
466
- *
467
- * See also:
468
- * - https://github.com/tc39/proposal-pipeline-operator/blob/main/README.md
469
- */
470
- for (const fn of fns)
471
- data = fn(data);
472
- return data;
473
- },
474
- async pipeAsync(data, ...fns) {
475
- /*
476
- * КАСТОМНЫЙ PIPE ОПЕРАТОР 2
477
- *
478
- * Никакой магии, обычный асинхронный |>
479
- * для получения и мутации данных сойдёт
480
- *
481
- * See also:
482
- * - https://github.com/tc39/proposal-pipeline-operator/blob/main/README.md
483
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
484
- */
485
- for (const fn of fns) {
486
- let waiter = await data;
487
- data = await fn(waiter);
488
- }
489
- return data;
490
- },
491
- form: {
492
- /*
493
- * АВТОСОХРАНЕНИЕ ФОРМ
494
- *
495
- * Позволяет сохранять состояние формы на случай
496
- * Если в офисе внезапно выключат свет
497
- *
498
- * ???: может сделать более полноценный модуль форм
499
- * с встроенной валидацией, или чем нибуть ещё
500
- *
501
- * See also:
502
- * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
503
- * - https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
504
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
505
- */
506
- read(form) {
507
- let data = {};
508
- new FormData(form).forEach((value, key)=>{
509
- if (data[key] !== undefined) {
510
- if (!Array.isArray(data[key]))
511
- data[key] = [data[key]];
512
- else
513
- data[key].push(value);
514
- } else
515
- data[key] = value;
516
- });
517
- return data;
518
- },
519
- write(form, data) {
520
- Object.entries(data).forEach(([key,value])=>{
521
- let el = form.elements[key];
522
- if (!el)
523
- return;
524
- if (el.length)
525
- [...el].forEach((opt,i)=>{
526
- let isCheckBox = 'selected';
527
- if (['checkbox','radio'].includes(opt.type))
528
- isCheckBox = 'checked';
529
-
530
- let select = false;
531
- if (Array.isArray(value)) {
532
- if (value.includes(opt.value))
533
- select = true;
534
- } else if (opt.value == value)
535
- select = true;
536
-
537
- opt[isCheckBox] = select;
538
- });
539
- else
540
- el.value = value;
541
- });
542
- return data;
543
- },
544
- },
545
- tables(elem, name, columns, raw, rowKey = 'ID') {
546
- /*
547
- * МОДУЛЬ АВТОТАБЛИЦ
548
- *
549
- * Позволяет быстро генерировать таблицы с особыми свойствами
550
- *
551
- * !!!: в columns параметр mutate работает как парсер значения
552
- * !!!: сортируйте сами путём мутации data, javascript как никак умеет
553
- * или вообще сортируйте на сервере
554
- *
555
- * ???: рассмотреть переделку апи т.к. в текущей реализации
556
- * гибкость все ещё слишком низкая
557
- */
558
- if (!Array.isArray(raw))
559
- raw = Object.values(raw);
560
- let state = {
561
- name: name,
562
- columns: columns,
563
- raw: raw,
564
- rowKey: rowKey,
565
- data: raw,
566
- elem: elem,
567
-
568
- render() {
569
- let html = '';
570
- html += `<thead><tr>`;
571
- for (let c of this.columns)
572
- html += `<th>${c.title || c.key}</th>`;
573
- html += `</tr></thead>`;
574
-
575
- html += `<tbody>`;
576
- for (let row of this.data) {
577
- let id = row[this.rowKey];
578
-
579
- html += `<tr data-id="${id}">`;
862
+ window[_].drag = {
863
+ /* МОДУЛЬ ДРАГГЕРА
864
+ * Author: MIOBOMB (2023-2026)
865
+ * Last patch: 2.1.7
866
+ *
867
+ * Самый обычный драггер, разве что адаптированный под движок окон
868
+ * Не буду врать, написал я его 2 года назад украв код с w3schools
869
+ * Но я провёл настолько глубокий рефакторинг что единственное напоминание:
870
+ * Алгоритм вычисления координат
871
+ *
872
+ * FIXME:
873
+ * Мультитач чувствует себя плохо на телефонах
874
+ * (возможно не только на них)
875
+ * Первые элементы начинают дрожжать и прыгать по экрану
876
+ * А последний взятый элемент сильно фризит
877
+ * Я что зря делал проброс nginx'а на 192.168.0.*?
878
+ * Непорядок
879
+ * HMM:
880
+ * оставить сломанный мультитач потому что
881
+ * мне лень его чинить + наврядли кто то
882
+ * всерьёз будет двигать много элементов за раз
883
+ *
884
+ * See also:
885
+ * - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX
886
+ * - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY
887
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
888
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
889
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
890
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event (почему нам нужен preventDefault)
891
+ * - https://www.w3schools.com/howto/howto_js_draggable.asp (основа для модуля)
892
+ */
893
+ _i: false,
894
+ active: new Map(),
580
895
 
581
- for (let c of this.columns) {
582
- let v = row[c.key];
583
- if (c.mutate)
584
- v = c.mutate(row);
585
- html += `<td>${v ?? ``}</td>`;
586
- }
587
- html += `</tr>`;
588
- }
589
- html += `</tbody>`;
590
-
591
- this.elem.innerHTML = `<table>${html}</table>`;
592
- }
593
- };
594
- return state;
595
- },
596
- storage: class {
597
- // See also:
598
- // - https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
599
- // - https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
600
- // - https://developer.mozilla.org/en-US/docs/Web/API/Storage
601
- constructor(storage, name) {
602
- this._ = storage;
603
- this.n = name;
604
- }
605
- get = key=> this._.getItem(this.n + key);
606
- set = (key, value)=>this._.setItem(this.n + key, value);
607
- remove = key=> this._.removeItem(this.n + key);
608
- clear = ()=>Object.keys(this._)
609
- .filter(k => k.startsWith(this.n))
610
- .forEach(k => this._.removeItem(k));
611
- },
612
- err: {
613
- //See also:
614
- // - https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
615
- // - https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
616
- // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
617
- init() {
618
- window.addEventListener('error',_.err.handleGlobal);
619
- window.addEventListener('unhandledrejection',_.err.handleRejection);
620
- },
621
- print: (cnt,e)=>console.error(e),
622
-
623
- errors: {},
624
- _c: 0,
625
- log(err) {
626
- _.err.print(_.err._c,err);
627
- _.err._c++;
628
- _.err.errors[_.err._c]=err;
629
- },
630
- handleGlobal(message,source,line,column,error){
631
- console.error(message,source+':'+line+':'+column,error)
632
- _.err.log(message + `\n IN ${source} ON LINE ${line} IN COLUMN ${column}`);
633
- },
634
- handleRejection(e){
635
- const err = e.reason || e;
636
- console.error(err);
637
- _.err.log(
638
- `PROMISE ERROR\n`+
639
- `${e.stack || e}`
640
- );
641
- },
642
- },
643
- hotkeys: {
644
- /*
645
- * ГОРЯЧИЕ КЛАВИШЫ
646
- *
647
- * Реализует самый настоящий press/release интерфейс
648
- * Если верить минификатору, после сжатия весит всего 790 байт
649
- *
650
- * В Object Hub уже есть текстовый редактор горячих клавиш
651
- * На базе этого движка, конечно давать textarea с js кодом...
652
- * Не самая безопасная затея, но как факт кастомизация широчайшая
653
- *
654
- * _holds работает не на массивах а на new Set()
655
- * Сеты работают намного быстрее при большом объёме данных
656
- * Вы же не хотите чтобы у вас тормозил поток с 100+ хоткеями
657
- * Из-за простого печатанья?
658
- *
659
- * FIXME: Рассмотреть альтернативы e.code из-за проблем
660
- * с otg-клавиатурами на телефонах
661
- *
662
- * See also:
663
- * - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
664
- * - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
665
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
666
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event
667
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
668
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
669
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
670
- */
671
- keys: new Map(),
672
- _holds: new Set(),
673
- _: false,
674
-
675
- _parse: combo => combo.split('+').map(k=>k.trim()),
676
- _match(keys) {
677
- // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
678
- for (let k of keys) if (!this._holds.has(k)) return false;
679
- return true;
680
- },
681
- _init() {
682
- if (this._)
683
- return;
684
- document.addEventListener('keydown', e=>{
685
- this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
686
- // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
687
-
688
- for (let hotkey of this.keys.values()) {
689
- if (!this._match(hotkey.keys))
690
- continue;
691
- if (hotkey.press && !hotkey.active) {
692
- hotkey.active = true; // active защищает от множественных срабатываний
693
- hotkey.press(e);
694
- }
695
- }
696
- });
697
- document.addEventListener('keyup', e=>{
698
- this._holds.delete(e.code);
699
-
700
- for (let hotkey of this.keys.values()) {
701
- if (hotkey.active && !this._match(hotkey.keys)) {
702
- hotkey.active=false;
703
- hotkey.release(e);
704
- }
705
- }
706
- });
707
- window.addEventListener('blur', e=>{
708
- /*
709
- * При переключении в другое окно автоматического keyup не будет
710
- * Поэтому сбрасываем всё принудительно, мало ли
711
- */
712
- for (let hotkey of this.keys.values()) {
713
- if (hotkey.active) {
714
- hotkey.active = false;
715
- hotkey.release();
716
- }
717
- }
718
- this._holds.clear();
719
- });
720
- this._=true;
721
- },
722
- on(combo, press, release) {
723
- this._init();
724
- let keys = this._parse(combo);
725
-
726
- this.keys.set(combo, {
727
- keys,
728
- // press/releace по умолчанию пустышки для сокращения синаксиса
729
- press: press || (()=>{}),
730
- release: release || (()=>{}),
731
- active: false
732
- });
733
-
734
- return this;
735
- },
736
- off(combo) {
737
- this.keys.delete(combo);
738
- return this;
739
- },
740
- },
741
- drag: {
742
- // See also:
743
- // - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX
744
- // - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY
745
- // - https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
746
- // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
747
- // - https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
748
- // - https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event (почему нам нужен preventDefault)
749
- _i: false,
750
- active: new Map(),
896
+ prevent: e=>e.target.closest('button,input'),
897
+ init(dragger, mover, onStart, onStop) {
898
+ let start=e=>{
899
+ // Проверяем куда нажали, если бы мы не проверяли,
900
+ // То драггер не дал бы нам нажать на кнопки или изменить имя окна
901
+ if (window[_].drag.prevent(e)) return;
902
+
903
+ e.preventDefault();
904
+
905
+ window[_].drag.active.set(e.pointerId,{
906
+ x:e.clientX,
907
+ y:e.clientY,
908
+ mover:mover,
909
+ onStop:onStop
910
+ });
911
+
912
+ onStart?.(e);
913
+ };
914
+ if (!window[_].drag._i) {
915
+ document.addEventListener("pointermove", (e) => window[_].drag.move(e));
916
+ document.addEventListener("pointerup", (e) => window[_].drag.stop(e));
917
+ document.addEventListener("pointercancel", (e) => window[_].drag.stop(e));
918
+ window[_].drag._i = true;
919
+ }
920
+ dragger.onpointerdown=start;
921
+ // превентим touchmove событие чтобы не было проблем
922
+ // при скролле на телефонах
923
+ dragger.ontouchmove=e=>e.preventDefault();
924
+ },
925
+ move(e) {
926
+ let p=window[_].drag.active.get(e.pointerId);
927
+ if(!p) return;
928
+ e.preventDefault();
929
+
930
+ let dx=p.x - e.clientX,
931
+ dy=p.y - e.clientY;
932
+
933
+ p.x=e.clientX;
934
+ p.y=e.clientY;
935
+
936
+ let mov = p.mover;
937
+ mov.style.top=(mov.offsetTop - dy)+"px";
938
+ mov.style.left=(mov.offsetLeft - dx)+"px";
939
+ },
940
+ stop(e) {
941
+ window[_].drag.active.get(e.pointerId)?.onStop?.(e);
942
+ window[_].drag.active.delete(e.pointerId);
943
+ },
944
+ };
751
945
 
752
- prevent: e=>e.target.closest('button,input'),
753
- init(dragger, mover, onStart, onStop) {
754
- let start=e=>{
755
- // Проверяем куда нажали, если бы мы не проверяли,
756
- // То драггер не дал бы нам нажать на кнопки или изменить имя окна
757
- if (this.prevent(e)) return;
758
-
759
- e.preventDefault();
760
-
761
- this.active.set(e.pointerId,{
762
- x:e.clientX,
763
- y:e.clientY,
764
- mover:mover,
765
- onStop:onStop
766
- });
767
-
768
- onStart?.(e);
769
- };
770
- if (!this._i) {
771
- document.addEventListener("pointermove", (e) => this.move(e));
772
- document.addEventListener("pointerup", (e) => this.stop(e));
773
- document.addEventListener("pointercancel", (e) => this.stop(e));
774
- this._i = true;
775
- }
776
- dragger.onpointerdown=start;
777
- // превентим touchmove событие чтобы не было проблем
778
- // при скролле на телефонах
779
- dragger.ontouchmove=e=>e.preventDefault();
780
- },
781
- move(e) {
782
- let p=this.active.get(e.pointerId);
783
- if(!p) return;
784
- e.preventDefault();
785
-
786
- let dx=p.x - e.clientX,
787
- dy=p.y - e.clientY;
788
-
789
- p.x=e.clientX;
790
- p.y=e.clientY;
791
-
792
- let mov = p.mover;
793
- mov.style.top=(mov.offsetTop - dy)+"px";
794
- mov.style.left=(mov.offsetLeft - dx)+"px";
795
- },
796
- stop(e) {
797
- this.active.get(e.pointerId)?.onStop?.(e);
798
- this.active.delete(e.pointerId);
799
- },
800
- },
801
- win:{
802
- /*
803
- * МОДУЛЬ ОКОН
804
- * если вы спросите почему ньюхелпер я отвечу
805
- * winBox.js это 35 килобайт, здесь же вы получаете в 25 килобайт
806
- * И более широкий движок окон и документацию уровня...
807
- * А у кого нибуть вообще есть такие подробные документации в вебе?
808
- *
809
- * Реализует ограниченно-гибкий движок окон, функционал:
810
- * - открытие, разворот на весь экран, закрытие
811
- * - сворачивание в таскбар и разворчаивание
812
- * - нативный css-ресайз (resize:both)
813
- * - возможность двигать окна (работает на телефонах, я проверял)
814
- * - сохранение и загрузка окон по вашему выбору
815
- *
816
- * теперь мне надо вспомнить я рефакторил этот код 4 раза или 7 раз
817
- *
818
- * !!!: _opn() и toggleFull() могут сломать ваши окна!
819
- * Эти функции высчитывают координаты окна, и размер окна с учётом padding'а
820
- * Ни за что не вешайте на ваши окна transform:translate()!
821
- *
822
- * !!!: _opn() по умолчанию открывает окно по центру экрана
823
- * Если не идёт восстановление через write()
824
- *
825
- * ???: стоит ли открывать окно в центре, или лучше дать "дефолтную функцию" позиционирования
826
- *
827
- * FIXME: вынести lang.winTitle из переводов для устранения "связности" кода
828
- *
829
- * See also:
830
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
831
- * - https://developer.mozilla.org/en-US/docs/Web/CSS/position
832
- * - https://developer.mozilla.org/en-US/docs/Web/CSS/resize
833
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event
834
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
835
- * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
836
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
837
- * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
838
- * - html модуль
839
- * - lang.winTitle функция
840
- * - drag модуль
841
- */
842
- manager:false,
843
- hider:false,
844
- text:'',
845
-
846
- winAttrs:'',
847
- dragAttrs:'',
848
- titleAttrs:'',
849
- renameAttrs:'',
850
- btnAttrs:'',
851
- hiderAttrs:'',
852
-
853
- defBtns:[
854
- ['–',w=>w.hide()],
855
- ['=',w=>w.toggleFull()],
856
- ['X',w=>w.close()],
857
- ],
858
-
859
- animOpen:'',
860
- animClose:'',
861
- animHide:'',
862
- animShow:'',
863
- animFullOn:'',
864
- animFullOff:'',
865
-
866
- _animate(elem, anim, actAfter = ()=>{}, actPre = ()=>{}) {
867
- if (anim) {
868
- elem.classList.add(anim);
869
- actPre();
870
- elem.addEventListener('animationend', ()=>{
871
- elem.classList.remove(anim);
872
- actAfter();
873
- }, { once: true });
874
- } else {
875
- actPre();
876
- actAfter();
877
- }
878
- },
879
- _ID(){
880
- let id;
881
- // Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
882
- // !!!: в теории можно задать любой айди
883
- // ???: проверить при скольки окнах генератор начинает тормозить
884
- do id=Math.random().toString(36).substring(2,8);
885
- while (_.wins.has(id));
886
- return id;
887
- },
888
- _winBtn(win,text,func){
889
- let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
890
- b.addEventListener('click',()=>func(win));
891
- return b;
892
- },
893
- _hiderBtn(win){
894
- let title=win.langs!== false ? _.lang.winTitle(_.win.text+win.langs) : `>${win.name}<`,
895
- b=_.html`<button id=hider${win.id} ${this.hiderAttrs}${title}/button>`;
896
- b.addEventListener('click',()=>this.show(win));
897
- return b;
898
- },
899
- _initWin: winState=>
900
- _.drag.init(winState.drag, winState.elem, ()=>_.win.manager.appendChild(winState.elem)),
901
- open(name,content='',customAttrs=''){
902
- let winId=this._ID(),
903
- winState={
904
- id:winId,
905
- name:name,
906
- langs:name,
907
- state:'opened',
908
- full:false,
909
- inRename:false,
910
- // Если окно новое, координаты полностью нулевые,
911
- // Нужно чтобы проверять создаётся ли окно и если да то задавать координаты
912
- onUnfull:{top:0,left:0,width:0,height:0},
913
- attrs:customAttrs,
914
- elem:false,
915
- drag:false,
916
- content:false,
917
- };
918
- return this._opn(winState,content);
919
- },
920
- _opn(winState,content=''){
921
- if (!this.manager || !this.hider) throw new Error('Window managers not inited');
922
-
923
- let wId=winState.id,
924
- html=
925
- _.html`<div id=${wId} ${this.winAttrs} ${winState.attrs}>
926
- <div style="display:flex;justify-content:space-between;align-items:center"
927
- ${this.dragAttrs} id=DRAGGER${wId}>
928
- <span ${this.titleAttrs} id=title${wId}${_.lang.winTitle(_.win.text+winState.name)}/span>
929
- <div id=btns${wId}></div>
930
- </div>
931
- <div id=content${wId} style=overflow:auto;width:100%;height:100%>
932
- ${content.replace(/\{winId\}/g,wId)}
933
- </div>
934
- </div>`,
935
- btns=html.querySelector(`#btns${wId}`);
936
- for(let b of this.defBtns) btns.append(this._winBtn(winState,...b));
937
- html.style.overflow='hidden';
938
- html.style.resize='both';
939
-
940
- this._animate(html, this.animOpen)
946
+ window[_].fade = function(elem, anim, actAfter = ()=>{}) {
947
+ if (!anim)
948
+ return actAfter();
949
+ elem.classList.add(anim);
950
+ elem.addEventListener('animationend', ()=>{
951
+ elem.classList.remove(anim);
952
+ actAfter();
953
+ }, { once: true });
954
+ };
941
955
 
942
- winState.setTitle=nT=>_.win.setTitle(winState,nT);
943
- winState.toggleFull=e=>_.win.toggleFull(winState);
944
- winState.close=e=>_.win.close(winState);
945
- winState.hide=e=>_.win.hide(winState);
946
- winState.show=e=>_.win.show(winState);
947
- this.manager.append(html);
948
-
949
- let win=winState.elem=document.getElementById(wId),
950
- contentRect=document.getElementById('content'+wId).getBoundingClientRect(),
951
- windowRect=win.getBoundingClientRect(),
952
- padX=windowRect.width - contentRect.width,padY=windowRect.height - contentRect.height;
953
- winState.drag=document.getElementById('DRAGGER'+wId);
954
- winState.content=document.getElementById('content'+wId);
955
-
956
- if (winState.onUnfull.width === 0) {
957
- // Здесь и задаются координаты...
958
- // Мастера клин кода не выносите мне мозги прошу
959
- // Оно же работает!!!
960
- if (!winState.attrs.includes('top')) {
961
- win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
962
- win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
963
- }
964
- if (!winState.attrs.includes('width')) win.style.height=(win.offsetHeight - padX) + 'px';
965
- if (!winState.attrs.includes('height')) win.style.width=(win.offsetWidth - padY) + 'px';
966
- } else
967
- for (let pos in winState.onUnfull)
968
- win.style[pos] = winState.onUnfull[pos] + 'px'
969
-
970
- this._initWin(winState);
971
- winState.drag.addEventListener('contextmenu',(e)=>{
972
- e.preventDefault();
973
- if(e.target.closest('button')) return;
974
- let wT=document.getElementById('title'+wId);
975
- if (!winState.inRename){
976
- wT.innerHTML=`<input ${this.renameAttrs} id=rename${wId} value="${wT.textContent}">`;
977
- winState.inRename=true;
978
- }else{
979
- this.setTitle(winState,document.getElementById('rename'+wId).value);
980
- winState.inRename=false;
981
- }
982
- });
983
-
984
- if (winState.state === 'hidened') winState.hide();
985
-
986
- _.wins.set(winState.id, winState);
987
- return winState;
988
- },
989
- setTitle(winState,newT){
990
- winState.langs=false;
991
- winState.name=newT;
992
- let t=document.getElementById('title'+winState.id),
993
- h=document.getElementById('hider'+winState.id);
994
- t.innerHTML=newT;
995
- t.removeAttribute('data-trans');
996
- if (h){
997
- h.innerHTML=newT;
998
- h.removeAttribute('data-trans');
999
- }
1000
- },
1001
- toggleFull(winState){
1002
- let wEl=winState.elem,
1003
- ws=wEl.style,
1004
- wc=wEl.classList,
1005
- contentRect=document.getElementById('content'+winState.id).getBoundingClientRect(),
1006
- windowRect=wEl.getBoundingClientRect(),
1007
- padX=windowRect.width - contentRect.width,
1008
- padY=windowRect.height - contentRect.height,
1009
- aOn=this.animFullOn,
1010
- aOff=this.animFullOff,
1011
- fd={
1012
- top: windowRect.top, left: windowRect.left,
1013
- width: contentRect.width, height: contentRect.height,
1014
- },
1015
- unful=()=>{
1016
- ws.top=old.top + 'px';
1017
- ws.left=old.left + 'px';
1018
- ws.width=old.width + 'px';
1019
- ws.height=old.height + 'px';
1020
- },
1021
- doFul=()=>{
1022
- if (aOn) wc.remove(aOn);
1023
- winState.full=true;
1024
- winState.onUnfull=fd;
1025
- ws.top=0;
1026
- ws.left=0;
1027
- ws.width=`calc(100% - ${padX}px)`;
1028
- ws.height=`calc(100% - ${padY}px)`;
1029
- winState.drag.onpointerdown=null;
1030
- },
1031
- doUnful=()=>{
1032
- if (aOff) wc.remove(aOff);
1033
- unful();
1034
- winState.full=false;
1035
- this._initWin(winState);
1036
- },
1037
- old=winState.onUnfull;
1038
- if (!winState.full)
1039
- this._animate(wEl, this.animFullOn, doFul)
1040
- else
1041
- this._animate(wEl, this.animFullOff, doUnful, unful)
1042
- },
1043
- close(winState){
1044
- let w=winState.elem,
1045
- remover=()=>{
1046
- let dr=winState.drag,D=document;
1047
- dr.onpointerdown=dr.ontouchmove=null;
1048
- // Удаляем обработчики висящие на документе
1049
- // Если их не удалять рано или поздно случится утечка памяти
1050
- // Я не знаю как я жил во времена 2.0 когда движок только появился
1051
- ['move','up','cancel'].map(e=>D['onpointer'+e]=null);
1052
- w.remove();
1053
- _.wins.delete(winState.id);
1054
- };
1055
- if (w.style.display== 'none') {
1056
- document.getElementById('hider'+winState.id).remove();
1057
- remover();
1058
- } else
1059
- this._animate(w, this.animClose, remover);
1060
-
1061
- },
1062
- hide(winState){
1063
- let wEl=winState.elem,
1064
- wc=wEl.classList,
1065
- anim=this.animHide,
1066
- hider=()=>{
1067
- wEl.style.display='none';
1068
- if(anim)wc.remove(anim);
1069
- winState.state='hidened';
1070
- this.hider.append(this._hiderBtn(winState));
1071
- }
1072
- this._animate(wEl, this.animHide, hider);
1073
- },
1074
- show(winState){
1075
- let wEl=winState.elem,
1076
- wc=wEl.classList,
1077
- anim=this.animShow,
1078
- hider=document.getElementById('hider'+winState.id),
1079
- shower=()=>{
1080
- if(anim)wc.remove(anim);
1081
- winState.state='opened';
1082
- }
1083
- wEl.style.display='';
1084
- hider.remove();
1085
- this._animate(wEl, this.animShow, shower);
1086
- },
1087
- /*
1088
- * о да, ниже идёт самая крутая фишка которую я готовлю к 2.2
1089
- *
1090
- * СОХРАНЕНИЕ-ВОССТАНОВКА ОКОН
1091
- * Помните автоформы? Здесь я поступил лучше
1092
- * Вы можете полностью сохранить окна, как - решаете вы, но лучше
1093
- * Вместо колбека я теперь просто делаю разовый читатель, так намного гибче
1094
- * Плюсом я делаю разовый восстановитель который возвращает все окна
1095
- * Так тоже в разы гибче, авось у вас в окнах были вебсокеты и их нужно восстановить
1096
- * Проще записать результат а потом прогнать проверку по data-ws атрибутам
1097
- * Или как вы ещё придумаете
1098
- *
1099
- * !!!: Оно работает настолько гибко что в теории можно сделать виртуальные рабочие столы
1100
- */
1101
- read(){
1102
- let store = {};
1103
- for (let [winId, winPre] of _.wins) {
1104
- let win = { ...winPre },
1105
- size=win.onUnfull,
1106
- wEl = win.elem,
1107
- contentRect=win.content.getBoundingClientRect(),
1108
- windowRect=wEl.getBoundingClientRect();
1109
- win.realContent=win.content.innerHTML;
1110
- size.top=windowRect.top;
1111
- size.left=windowRect.left;
1112
- size.height=wEl.offsetHeight - (windowRect.height - contentRect.height);
1113
- size.width=wEl.offsetWidth - (windowRect.width - contentRect.width);
1114
- delete win.elem;
1115
- delete win.drag;
1116
- delete win.content;
1117
- store[winId] = win;
1118
- }
1119
- return store;
1120
- },
1121
- write(state){
1122
- for (let winId in state) {
1123
- let win=state[winId],
1124
- content=win.realContent;
1125
- delete win.realContent;
1126
- _.wins.set(winId, win);
1127
- this._opn(win,content);
1128
- }
1129
- return _.wins;
1130
- },
1131
- },
1132
- wins: new Map(),
1133
- };
1134
-
1135
- return _
956
+ window[_].x10 = {
957
+ /*
958
+ * МОДУЛЬ ОКОН (РЕАЛЬНЫЙ)
959
+ * Author: MIOBOMB (2026)
960
+ * Last patch: 2.1.8
961
+ *
962
+ * Помните прошлый и уродливый win?
963
+ * Я теперь понял что насрал говна и решил подсмотреть
964
+ * архитектуру у гениев своего времени.
965
+ * По итогу я создал это чудо юдо чтобы поверх него строить "оконные менеджеры"!
966
+ * Прошлый модуль win все ещё останется рабочим и возможно
967
+ * будет встроенным wm как это сделано в реальных иксах с toms window manager.
968
+ * В отличии от реального x.org у меня будет реализована возможность
969
+ * исполнять несколько оконных менеджеров в одной сессии,
970
+ * нужно мне это чтобы модуль toast мог просто создавать для каждого
971
+ * инстанса тостов свой очень тонкий wm.
972
+ *
973
+ * СОСТОЯНИЯ ОКОН
974
+ * Это вообще отдельная тема, которую мне стоит осветить особенно от x10.
975
+ * Дело в том, что я вам дарую свободу обзывать и вешать в состояния окон
976
+ * всё что вам вздумается! Только пожалуйста, давайте не будем творить что попало
977
+ * и мы примим этот "контракт" о выдаче названий состояний окон:
978
+ *
979
+ * opened - окно открыто и видно
980
+ * hidened - окно скрыто
981
+ * opening - окно в анимации открытия
982
+ * hiding - окно в анимации сворачивания
983
+ * showing - окно в анимации разворачивания
984
+ * moving - окно в анимации движения
985
+ * closing - окно входит в анимацию закрытия и скоро умрёт
986
+ *
987
+ * Да, я сам эти состояния пока не использую в win, но к какой нибуть 2.4
988
+ * я начну над этим работать
989
+ * FIXME: прописать newHelper win адекватную работу со всеми состояниями
990
+ * FIXME: создать x10.iterate функцию
991
+ *
992
+ * !!!: это x.org! Тупая софтина которая просто хранит окна и плюёт события!
993
+ * Управлять окнами и DOM'ом окон все ещё будет оконный менеджер!
994
+ *
995
+ * See also:
996
+ * - https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
997
+ * - https://en.wikipedia.org/wiki/X_Window_System (будет полезно понимать)
998
+ */
999
+ busName: 'xws:',
1000
+
1001
+ generateId() {
1002
+ let id;
1003
+ // Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
1004
+ // !!!: в теории можно задать любой айди
1005
+ // HMM: проверить при скольки окнах генератор начинает тормозить
1006
+ do id=Math.random().toString(36).substring(2,8);
1007
+ while (window[_].wins[id]);
1008
+ //while (window[_].wins.has(id));
1009
+ return id;
1010
+ },
1011
+
1012
+ // метод spit() публичный, если вам нужны какие то
1013
+ // специфичные события для вашего WM - пожалуйста
1014
+ // HMM: дать ли ему нормальное название или нет
1015
+ spit(evName, data) {
1016
+ let event = new CustomEvent(window[_].x10.busName + evName, data)
1017
+ document.dispatchEvent(event);
1018
+ },
1019
+
1020
+ create(name, patcher) {
1021
+ let winId = window[_].x10.generateId(),
1022
+ state = {
1023
+ id: winId,
1024
+ name: name,
1025
+ state: 'opened',
1026
+ wm: '',
1027
+ };
1028
+
1029
+ // оконные менеджеры могут вешать свои флаги в окна
1030
+ Object.assign(state, patcher);
1031
+ window[_].wins[winId] = state;
1032
+ window[_].x10.spit('created', window[_].wins[winId]);
1033
+
1034
+ return window[_].wins[winId];
1035
+ },
1036
+
1037
+ get(winId) {
1038
+ return window[_].wins[winId];
1039
+ },
1040
+
1041
+ // vim ciw отсылка лол
1042
+ ciw(winId, attr, value, events = false) {
1043
+ let winData = window[_].wins[winId];
1044
+ winData[attr] = value;
1045
+ if (events) {
1046
+ window[_].x10.spit('patched', {[attr]: value});
1047
+ window[_].x10.spit('patched:' + attr, value);
1048
+ }
1049
+ },
1050
+
1051
+ setState(winId, state) {
1052
+ let winData = window[_].wins[winId];
1053
+ winData.state = state;
1054
+ window[_].x10.spit('state:' + state, winData);
1055
+ return winData;
1056
+ },
1057
+
1058
+ destroy(winId) {
1059
+ window[_].x10.spit('destroyed', window[_].wins[winId]);
1060
+ delete window[_].wins[winId];
1061
+ },
1062
+ };
1063
+
1064
+ window[_].toast = class {
1065
+ /*
1066
+ * МОДУЛЬ УВЕДОМЛЕНИЙ
1067
+ * Author: MIOBOMB (2026)
1068
+ * Last patch: 2.1.8
1069
+ *
1070
+ * Является фабрикой оконных менеджеров, работает по такому принципу:
1071
+ * errAlert = new window[_].toast('error', DOMelem)
1072
+ * okAlert = new window[_].toast('ok', DOMelem)
1073
+ * loadingAnim = new window[_].toast('loading', DOMelem, 0)
1074
+ *
1075
+ * Сам тонкий оконный менеджер крайне примитивный и простой,
1076
+ * но для тостов обычно большего и не надо.
1077
+ *
1078
+ * Вы также можете переопределить метод generateDOM
1079
+ * под ваши нужны. В целом, это безопасно если иметь меру.
1080
+ * в Object hub это уже используется повсеместно.
1081
+ *
1082
+ * !!!:
1083
+ * если duration = 0 то тост будет закрыт только при вызове close()
1084
+ * !!!:
1085
+ * не вешайте manager'ом document.body!
1086
+ * управление позицией окон отдано на откуп DOM и CSS,
1087
+ * вам придётся настроить элемент менеджера и классы или стили
1088
+ * тостов, чтобы они позиционировались адекватно
1089
+ */
1090
+
1091
+ constructor(name, manager, duration = 3000, attrs = '', animOpen = '', animClose = '') {
1092
+ this.name = name;
1093
+ this.wm = 'newHelper-toast-' + this.name;
1094
+ this.manager = manager;
1095
+ this.duration = duration;
1096
+ this.attrs = attrs;
1097
+ this.animOpen = animOpen;
1098
+ this.animClose = animClose;
1099
+ }
1100
+ generateDOM = (wId,content) => `<div style=overflow:auto;width:100%;height:100%>${content}</div>`.replace(/\{winId\}/g,wId)
1101
+
1102
+ open(content = '') {
1103
+ let winState = window[_].x10.create(this.name, { wm: this.wm }),
1104
+ wId = winState.id,
1105
+ html = window[_].html`<div id=${wId} ${this.attrs}>
1106
+ <div style="display:flex;justify-content:space-between;align-items:center">
1107
+ ${this.generateDOM(wId, content)}
1108
+ </div>
1109
+ </div>`;
1110
+ window[_].fade(html, this.animOpen);
1111
+ this.manager.append(html);
1112
+
1113
+ // сохраним ссылку на dom чтобы достать её в close()
1114
+ // HMM: а может писать в this.elem
1115
+ window[_].x10.ciw(wId, 'elem', document.getElementById(wId));
1116
+
1117
+ if (this.duration > 0)
1118
+ setTimeout(() => this.close(wId), this.duration)
1119
+ }
1120
+ close(winId) {
1121
+ let win = window[_].x10.get(winId), w;
1122
+ if (!win || win.wm !== this.wm)
1123
+ return;
1124
+ w = win.elem;
1125
+ window[_].fade(w, this.animClose, ()=>{
1126
+ w.remove();
1127
+ window[_].x10.destroy(winId);
1128
+ });
1129
+ }
1130
+ };
1131
+
1132
+ window[_].win = {
1133
+ /*
1134
+ * МОДУЛЬ ОКОН
1135
+ * Author: MIOBOMB (2023-2026)
1136
+ * Last patch: 2.1.8
1137
+ *
1138
+ * если вы спросите почему ньюхелпер я отвечу
1139
+ * winBox.js это 35 килобайт, здесь же вы получаете в 65 килобайт
1140
+ * И более мощный движок окон и документацию уровня...
1141
+ * А у кого нибуть вообще есть такие подробные документации в вебе?
1142
+ *
1143
+ * Реализует ограниченно-гибкий движок окон, функционал:
1144
+ * - открытие, разворот на весь экран, закрытие
1145
+ * - сворачивание в таскбар и разворчаивание
1146
+ * - нативный css-ресайз (resize:both)
1147
+ * - возможность двигать окна (работает на телефонах, я проверял)
1148
+ * - сохранение и загрузка окон по вашему выбору
1149
+ *
1150
+ * теперь мне надо вспомнить я рефакторил этот код 4 раза или 7 раз
1151
+ * 25:06:2026 - Вспомнил! раз 6 точно, +x10 значит это уже 7 раз
1152
+ *
1153
+ * !!!: _opn() и toggleFull() могут сломать ваши окна!
1154
+ * Эти функции высчитывают координаты окна, и размер окна с учётом padding'а
1155
+ * Ни за что не вешайте на ваши окна transform:translate()!
1156
+ *
1157
+ * !!!: _opn() по умолчанию открывает окно по центру экрана
1158
+ * Если не идёт восстановление через write()
1159
+ *
1160
+ * HMM: стоит ли открывать окно в центре, или лучше дать "дефолтную функцию" позиционирования
1161
+ *
1162
+ * FIXME: вынести lang.winTitle из переводов для устранения "связности" кода
1163
+ *
1164
+ * FIXME: придумать что делать с переездом window[_].wins на new Map
1165
+ * я уже пробовал перенос на мапу и результат...
1166
+ * у меня сломались все окна в object hub'е
1167
+ * по этому пусть пока хоть до 2.5 будет объект
1168
+ *
1169
+ * See also:
1170
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
1171
+ * - https://developer.mozilla.org/en-US/docs/Web/CSS/position
1172
+ * - https://developer.mozilla.org/en-US/docs/Web/CSS/resize
1173
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event
1174
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
1175
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
1176
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
1177
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
1178
+ * - html модуль
1179
+ * - lang.winTitle функция
1180
+ * - drag модуль
1181
+ * - x10 подсистема
1182
+ */
1183
+ manager:false,
1184
+ hider:false,
1185
+ text:'',
1186
+
1187
+ winAttrs:'',
1188
+ dragAttrs:'',
1189
+ titleAttrs:'',
1190
+ renameAttrs:'',
1191
+ btnAttrs:'',
1192
+ hiderAttrs:'',
1193
+
1194
+ defBtns:[
1195
+ ['–',w=>w.hide()],
1196
+ ['=',w=>w.toggleFull()],
1197
+ ['X',w=>w.close()],
1198
+ ],
1199
+
1200
+ animOpen:'',
1201
+ animClose:'',
1202
+ animHide:'',
1203
+ animShow:'',
1204
+ animFullOn:'',
1205
+ animFullOff:'',
1206
+
1207
+
1208
+ _winBtn(win,text,func){
1209
+ let b=window[_].html`<button ${window[_].win.btnAttrs}>${text}</button>`;
1210
+ b.addEventListener('click',()=>func(win));
1211
+ return b;
1212
+ },
1213
+ _hiderBtn(win){
1214
+ let title=win.langs!== false ? window[_].lang.winTitle(window[_].win.text+win.langs) : `>${win.name}<`,
1215
+ b=window[_].html`<button id=hider${win.id} ${window[_].win.hiderAttrs}${title}/button>`;
1216
+ b.addEventListener('click',()=>window[_].win.show(win));
1217
+ return b;
1218
+ },
1219
+ _initWin: winState=>
1220
+ window[_].drag.init(winState.drag, winState.elem, ()=>window[_].win.manager.appendChild(winState.elem)),
1221
+
1222
+ open(name,content='',customAttrs=''){
1223
+ let winState = {
1224
+ wm: 'newHelper-win',
1225
+ langs:name,
1226
+ full:false,
1227
+ inRename:false,
1228
+ // Если окно новое, координаты полностью нулевые,
1229
+ // Нужно чтобы проверять создаётся ли окно и если да то задавать координаты
1230
+ onUnfull:{top:0,left:0,width:0,height:0},
1231
+ attrs:customAttrs,
1232
+ elem:false,
1233
+ drag:false,
1234
+ content:false,
1235
+
1236
+ setTitle(nT) {window[_].win.setTitle(this,nT)},
1237
+ toggleFull(e) {window[_].win.toggleFull(this)},
1238
+ close(e) {window[_].win.close(this)},
1239
+ hide(e) {window[_].win.hide(this)},
1240
+ show(e) {window[_].win.show(this)},
1241
+ },
1242
+ winData = window[_].x10.create(name, winState);
1243
+ return window[_].win._opn(winData,content);
1244
+ },
1245
+ _opn(winState,content=''){
1246
+ // эти проверки жрут драгоценные байты
1247
+ // if (!window[_].win.manager || !window[_].win.hider) throw new Error('Window managers not inited');
1248
+ let wId=winState.id,
1249
+ html=
1250
+ window[_].html`<div id=${wId} ${window[_].win.winAttrs} ${winState.attrs}>
1251
+ <div style="display:flex;justify-content:space-between;align-items:center"
1252
+ ${window[_].win.dragAttrs} id=DRAGGER${wId}>
1253
+ <span ${window[_].win.titleAttrs} id=title${wId}${window[_].lang.winTitle(window[_].win.text+winState.name)}/span>
1254
+ <div id=btns${wId}></div>
1255
+ </div>
1256
+ <div id=content${wId} style=overflow:auto;width:100%;height:100%>
1257
+ ${content.replace(/\{winId\}/g,wId)}
1258
+ </div>
1259
+ </div>`,
1260
+ btns = html.querySelector(`#btns${wId}`);
1261
+ for(let b of window[_].win.defBtns) btns.append(window[_].win._winBtn(winState,...b));
1262
+ html.style.overflow = 'hidden';
1263
+ html.style.resize = 'both';
1264
+
1265
+ window[_].fade(html, window[_].win.animOpen)
1266
+
1267
+ window[_].win.manager.append(html);
1268
+
1269
+ // AFTER INSERT LOGISC !!!!!!!!!!!!!!!!
1270
+
1271
+ let mainElem = document.getElementById(wId),
1272
+ dragElem = document.getElementById('DRAGGER'+wId),
1273
+ contentElem = document.getElementById('content'+wId);
1274
+
1275
+ let win = winState.elem = mainElem,
1276
+ contentRect = document.getElementById('content'+wId).getBoundingClientRect(),
1277
+ windowRect = win.getBoundingClientRect(),
1278
+ padX=windowRect.width - contentRect.width,padY=windowRect.height - contentRect.height;
1279
+ winState.drag = dragElem,
1280
+ winState.content = contentElem;
1281
+
1282
+ if (winState.onUnfull.width === 0) {
1283
+ // Здесь и задаются координаты...
1284
+ // Мастера клин кода не выносите мне мозги прошу
1285
+ // Оно же работает!!!
1286
+ // 26:06:2026 - и вроде гзипается хорошо
1287
+ if (!winState.attrs.includes('top')) {
1288
+ win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
1289
+ win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
1290
+ }
1291
+ if (!winState.attrs.includes('width'))
1292
+ win.style.width=(win.offsetWidth - padY) + 'px';
1293
+ if (!winState.attrs.includes('height'))
1294
+ win.style.height=(win.offsetHeight - padX) + 'px';
1295
+ } else
1296
+ for (let pos in winState.onUnfull)
1297
+ win.style[pos] = winState.onUnfull[pos] + 'px'
1298
+
1299
+ dragElem.addEventListener('contextmenu', function(e) {
1300
+ e.preventDefault();
1301
+ if (e.target.closest('button')) return;
1302
+ let wT = document.getElementById('title'+wId);
1303
+ if (!winState.inRename){
1304
+ wT.innerHTML=`<input ${window[_].win.renameAttrs} id=rename${wId} value="${wT.textContent}">`;
1305
+ window[_].x10.ciw(wId,'inRename',true);
1306
+ //winState.inRename=true;
1307
+ } else {
1308
+ window[_].win.setTitle(winState,document.getElementById('rename'+wId).value);
1309
+ window[_].x10.ciw(wId,'inRename',false);
1310
+ //winState.inRename=false;
1311
+ }
1312
+ });
1313
+
1314
+ if (winState.state === 'hidened')
1315
+ winState.hide();
1316
+ else
1317
+ window[_].win._initWin(winState);
1318
+ return winState;
1319
+ },
1320
+
1321
+ setTitle(winState, newT) {
1322
+ window[_].x10.ciw(winState.id,'langs',false);
1323
+ window[_].x10.ciw(winState.id,'name',newT);
1324
+ let t=document.getElementById('title'+winState.id),
1325
+ h=document.getElementById('hider'+winState.id);
1326
+ t.innerHTML=newT;
1327
+ t.removeAttribute('data-trans');
1328
+ if (h) {
1329
+ h.innerHTML=newT;
1330
+ h.removeAttribute('data-trans');
1331
+ }
1332
+ },
1333
+
1334
+ toggleFull(winState) {
1335
+ let wEl = winState.elem,
1336
+ ws = wEl.style,
1337
+ wc = wEl.classList,
1338
+ contentRect = document.getElementById('content'+winState.id).getBoundingClientRect(),
1339
+ windowRect = wEl.getBoundingClientRect(),
1340
+ padX = windowRect.width - contentRect.width,
1341
+ padY = windowRect.height - contentRect.height,
1342
+ aOn = window[_].win.animFullOn,
1343
+ aOff = window[_].win.animFullOff,
1344
+ fd = {
1345
+ top: windowRect.top, left: windowRect.left,
1346
+ width: contentRect.width, height: contentRect.height,
1347
+ },
1348
+ unful = ()=>{
1349
+ ws.top = old.top + 'px';
1350
+ ws.left = old.left + 'px';
1351
+ ws.width = old.width + 'px';
1352
+ ws.height = old.height + 'px';
1353
+ },
1354
+ doFul = ()=>{
1355
+ if (aOn) wc.remove(aOn);
1356
+ window[_].x10.ciw(winState.id,'full',true);
1357
+ window[_].x10.ciw(winState.id,'onUnfull',fd);
1358
+ ws.top = 0;
1359
+ ws.left = 0;
1360
+ ws.width = `calc(100% - ${padX}px)`;
1361
+ ws.height = `calc(100% - ${padY}px)`;
1362
+ winState.drag.onpointerdown = null;
1363
+ },
1364
+ doUnful = ()=>{
1365
+ if (aOff) wc.remove(aOff);
1366
+ unful();
1367
+ window[_].x10.ciw(winState.id,'full',false);
1368
+ window[_].win._initWin(winState);
1369
+ },
1370
+ old = winState.onUnfull;
1371
+ if (!winState.full)
1372
+ window[_].fade(wEl, window[_].win.animFullOn, doFul)
1373
+ else {
1374
+ unful();
1375
+ window[_].fade(wEl, window[_].win.animFullOff, doUnful);
1376
+ }
1377
+ },
1378
+
1379
+ close(winState) {
1380
+ let w = winState.elem,
1381
+ remover = ()=>{
1382
+ let dr = winState.drag;
1383
+ dr.onpointerdown = dr.ontouchmove=null;
1384
+ w.remove();
1385
+ window[_].x10.destroy(winState.id);
1386
+ };
1387
+ if (w.style.display== 'none') {
1388
+ document.getElementById('hider'+winState.id).remove();
1389
+ remover();
1390
+ } else
1391
+ window[_].fade(w, window[_].win.animClose, remover);
1392
+ },
1393
+
1394
+ hide(winState) {
1395
+ let wEl = winState.elem,
1396
+ wc = wEl.classList,
1397
+ anim = window[_].win.animHide,
1398
+ hider = ()=>{
1399
+ wEl.style.display = 'none';
1400
+ if (anim) wc.remove(anim);
1401
+ window[_].x10.setState(winState.id,'hidened');
1402
+ window[_].win.hider.append(window[_].win._hiderBtn(winState));
1403
+ }
1404
+ window[_].fade(wEl, window[_].win.animHide, hider);
1405
+ },
1406
+
1407
+ show(winState) {
1408
+ let wEl = winState.elem,
1409
+ wc = wEl.classList,
1410
+ anim = window[_].win.animShow,
1411
+ hider = document.getElementById('hider'+winState.id),
1412
+ shower = ()=>{
1413
+ if (anim) wc.remove(anim);
1414
+ window[_].x10.setState(winState.id,'opened');
1415
+ }
1416
+ wEl.style.display = '';
1417
+ hider.remove();
1418
+ window[_].fade(wEl, window[_].win.animShow, shower);
1419
+ },
1420
+
1421
+ /*
1422
+ * о да, ниже идёт самая крутая фишка которую я готовлю к 2.2
1423
+ *
1424
+ * СОХРАНЕНИЕ-ВОССТАНОВКА ОКОН
1425
+ * Помните автоформы? Здесь я поступил лучше
1426
+ * Вы можете полностью сохранить окна, как - решаете вы, но лучше
1427
+ * Вместо колбека я теперь просто делаю разовый читатель, так намного гибче
1428
+ * Плюсом я делаю разовый восстановитель который возвращает все окна
1429
+ * Так тоже в разы гибче, авось у вас в окнах были вебсокеты и их нужно восстановить
1430
+ * Проще записать результат а потом прогнать проверку по data-ws атрибутам
1431
+ * Или как вы ещё придумаете
1432
+ *
1433
+ * !!!: Оно работает настолько гибко что в теории можно сделать виртуальные рабочие столы
1434
+ *
1435
+ * FIXME: добавить проверку принадлежности WM
1436
+ */
1437
+ read(){
1438
+ let store = {};
1439
+ for (let winId in window[_].wins) {
1440
+ let winPre = window[_].wins[winId];
1441
+ //for (let [winId, winPre] of window[_].wins) {
1442
+ let win = { ...winPre },
1443
+ size=win.onUnfull,
1444
+ wEl = win.elem,
1445
+ contentRect=win.content.getBoundingClientRect(),
1446
+ windowRect=wEl.getBoundingClientRect();
1447
+ win.realContent=win.content.innerHTML;
1448
+ size.top=windowRect.top;
1449
+ size.left=windowRect.left;
1450
+ size.height=wEl.offsetHeight - (windowRect.height - contentRect.height);
1451
+ size.width=wEl.offsetWidth - (windowRect.width - contentRect.width);
1452
+ delete win.elem;
1453
+ delete win.drag;
1454
+ delete win.content;
1455
+ store[winId] = win;
1456
+ }
1457
+ return store;
1458
+ },
1459
+ write(state){
1460
+ for (let winId in state) {
1461
+ let win=state[winId],
1462
+ content=win.realContent;
1463
+ delete win.realContent;
1464
+ window[_].wins[winId] = win;
1465
+ //window[_].wins.set(winId, win);
1466
+ window[_].win._opn(win,content);
1467
+ }
1468
+ return window[_].wins;
1469
+ },
1470
+ };
1471
+
1472
+ window[_].wins = {};
1473
+ //wins: new Map(),
1474
+
1475
+ return window[_];
1136
1476
  };
1477
+
1478
+ /* полифиллы к удалённым модулям
1479
+ * (сделаны в формате плагинов)
1480
+ * DOM хелпер ($), удалён в 2.1.X:
1481
+ window[_].$ = {
1482
+ D: document,
1483
+ id: i=> document.getElementById(i),
1484
+ q: (i,p=document)=> p.querySelector(i),
1485
+ qa: (i,p=document)=> p.querySelectorAll(i),
1486
+
1487
+ on: (el,ev,fn,opts)=> el.addEventListener(ev,fn,opts),
1488
+ off: (el,ev,fn,opts)=> el.removeEventListener(ev,fn,opts),
1489
+
1490
+ cliRect: e=> e.getBoundingClientRect(), // сокращение чтобы не писать 25+ символов
1491
+ }
1492
+ * Причины удаления:
1493
+ * 1. в процессе разработки 2.2 я понял что мне не нужен
1494
+ * "сверхлегкий исходный код", а нужен крайне сжатый min+gzip
1495
+ * 2. в следствие пункта 1 я удалил весь синтаксический сахар
1496
+ * потому что "document." гзипается заметно лучше
1497
+ */