newhelper-js 2.1.2 → 2.1.5

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 (7) hide show
  1. package/LICENSE +24 -24
  2. package/README-en.md +27 -27
  3. package/README.md +33 -28
  4. package/docs.md +283 -283
  5. package/history.md +31 -31
  6. package/newHelper.js +1042 -870
  7. package/package.json +45 -45
package/newHelper.js CHANGED
@@ -1,870 +1,1042 @@
1
- /*
2
- * Стиль комментариев
3
- * FIXME - странное поведение функции, которое желательно бы переделать
4
- * ??? - требует уточнения
5
- * !!! - обратите внимание
6
- *
7
- * Перед вами код newHelper.js версии 2.1.2, он построен на базе гибкой псевдофабрики
8
- * Которая начинается с _=function(){...}();
9
- * Если у вас есть конфликты с Lodash вы можете переименовать _ на всё что вам нужно
10
- *
11
- * НЕСТАБИЛЬНОЕ API (НЕ РЕКОМЕНДУЕТСЯ ДЛЯ PRODUCTION):
12
- * модуль _.autoForm
13
- * _.win.read()
14
- * _.win.write()
15
- *
16
- * УДАЛЕНО (вернётся позже):
17
- * модуль _.err
18
- */
19
-
20
-
21
- _=function(){let _={
22
- link:{
23
- /*
24
- * МОДУЛЬ ССЫЛОК
25
- *
26
- * Работает по принципу [ссылка, команды...]
27
- * Пример: ?page=home&debug&lang=ru
28
- * ^^^^^^^^ ^^^^^ ^^^^^^
29
- * страница команды
30
- *
31
- * В процессе разработки ядра 2.0 в Object hub я понял
32
- * Что команды могут быть очень полезными для отладки
33
- * Но в теории на них можно повешать все модальные и прочие действия
34
- *
35
- * !!!: в функции get() работает весь роутинг, в т.ч. вложенный для страниц
36
- */
37
- basePage:()=>{},
38
- defTitle:'',
39
- actions:{},
40
- commands:{},
41
-
42
- _i: true, // _i - блокировщик pushState в set()
43
- _cmd:[],
44
- compile:()=>location.search.replace('?','').split('&'),
45
- set(page,title=this.defTitle){
46
- if (title) _.$.D.title=title;
47
- if (!this._i){
48
- let link=this.compile();
49
- link[0]=page;
50
- history.pushState(null,null,'?'+link.join('&'));
51
- }
52
- this._i=false;
53
- },
54
- add(cmd){
55
- let link=this.compile();
56
- if (!link.includes(cmd)){
57
- link.push(cmd);
58
- this._cmd.push(cmd);
59
- history.replaceState(null,null,'?'+link.join('&'));
60
- }
61
- },
62
- remove(cmd){
63
- let link=this.compile();
64
- if (link.includes(cmd)){
65
- let c=this._cmd;
66
- link.splice(link.indexOf(cmd),1);
67
- c.splice(c.indexOf(cmd),1);
68
- history.replaceState(null,null,'?'+link.join('&'));
69
- }
70
- },
71
- get(){
72
- /*
73
- * Страницы бросают ошибку чтобы вызвать базовую страницу
74
- * Команды тем временем так не делают
75
- * Потому что сломанная команда не так страшна как сломанная страница
76
- *
77
- * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
78
- * Сделано это для переноса команд при прыжках по истории
79
- */
80
- let links=this.compile(),
81
- [firstKey,fisrtValue]=links[0].split('='),
82
- cmds=links.slice(1);
83
- try {
84
- let dirs=firstKey.split('/'),
85
- dir=this.actions,
86
- main=dir[firstKey];
87
- if (!firstKey.includes('/')){
88
- main(fisrtValue);
89
- } else {
90
- for (let p of dirs){
91
- let kDir=dir[p+'/'];
92
- if (kDir)
93
- dir=kDir;
94
- else{
95
- dir[p](fisrtValue);
96
- break;
97
- }
98
- }
99
- }
100
- }catch(e){
101
- this.basePage();
102
- throw e;
103
- }
104
- this._cmd=cmds;
105
- cmds.forEach(cmmdPre=>{
106
- let [key,value]=cmmdPre.split('=');
107
- let cmd=this.commands[key];
108
- if (cmd) cmd(value);
109
- else console.error(new Error(`command '${cmd}' doesn't exist!`))
110
- });
111
- },
112
- },
113
- lazy:{
114
- /*
115
- * МОДУЛЬ ЛЕНИ
116
- *
117
- * Создаёт в глобальной области видимости прокси функции
118
- * Вызывающие загрузку скрипта с внещним модулем
119
- * Был сделан через глобальную область, так намного проще создавать лень
120
- *
121
- * !!!: Функции обёртки в register() должны быть повешаны на window
122
- * Иначе lazy._ провалится в рекурсию ошибок, не наступайте на мои грабли
123
- *
124
- * ???: будет ли легче создавать лень в легаси проектах через es6 импорты
125
- */
126
- loaded:{},
127
- load(url,...args){
128
- let key=url.split('?')[0], // отсекаем параметры, чтобы не дублировать
129
- state=this.loaded,
130
- c=state[key];
131
-
132
- /*
133
- * ...args передаются в Promise.resolve(args)
134
- * Это позволяет делать _.lazy.load('script.js', 'данные', 'для', 'колбека')
135
- * И потом в .then((a,b,c)=>...) получать эти аргументы
136
- *
137
- * Тройное состояние скрипта в lazy.loaded:
138
- * - true: уже загружен => сразу резолвим
139
- * - Promise: грузится сейчас => ждём тот же промис
140
- * - undefined: ещё не грузили => создаём новый промис
141
- *
142
- * Это защита от двойной загрузки одного скрипта
143
- */
144
- if (c=== true) return Promise.resolve(args);
145
- if (c instanceof Promise) return c.then(()=>args);
146
-
147
- let pr=new Promise((resolve,reject)=>{
148
- let scr=_.$.D.createElement('script');
149
- scr.src=url;
150
- scr.onload=()=>{
151
- state[key]=true;
152
- resolve(args);
153
- };
154
- scr.onerror=()=>{
155
- delete state[key];
156
- reject(new Error('Failed to load '+url));
157
- };
158
- _.$.D.head.append(scr);
159
- });
160
- state[key]=pr;
161
- return pr;
162
- },
163
- register(scr,funcs){
164
- if (!Array.isArray(funcs)) return new Error('Array required for register');
165
-
166
- for (let fn of funcs)
167
- window[fn]=(...a)=>this._(scr,fn).then(f=>f(...a));
168
- // ???: добавить вложенность с созданием объектов-обёрток
169
-
170
- console.info('lazy> Applied lazy '+scr+' with this functions:',funcs);
171
- // ???: не мешает ли тут console.info
172
- },
173
- async _(scr,fn){
174
- let w=window,
175
- wrapper=w[fn];
176
- try{await this.load(scr)}
177
- catch(e){throw e}
178
- if (wrapper!== w[fn]) return w[fn];
179
- throw new Error(`Function ${fn} not loaded from ${scr}`);
180
- },
181
- },
182
- lang:{
183
- /*
184
- * МОДУЛЬ ПЕРЕВОДОВ
185
- *
186
- * По слухам этот модуль лучше чем многие i18n реализации
187
- * Всё потому что он из коробки умеет переводить страницу без перезагрузки
188
- *
189
- * !!!: parse() обрабатывает ключи из vars и подставляет их значения
190
- * ваш +ключ+ становится значением, и это значение динамичное
191
- * Так удобнее отображать динамичные данные на сайтах
192
- * Например никнейм пользователя
193
- */
194
- addr:'',
195
- vars:{},
196
- main:{},
197
-
198
- load(name){
199
- return new Promise((resolve,reject)=>{
200
- _.http.req('GET',this.addr+name+'.json',false,{'Cache-Control':'no-cache,no-store,max-age=0'})
201
- .then(data=>resolve(data));
202
- })
203
- // ???: убрать ли захардкоженое отключение кеша
204
- },
205
- parse:(packet,vars=_.lang.vars)=>
206
- /*
207
- * Регулярка ищет всё, что внутри плюсов, в JSON они редки
208
- * Это позволяет без конфликтов подставлять переменные если они нашлись
209
- *
210
- * ???: переделать под общий синтаксис типа {var}
211
- */
212
- packet.replace(/\+([^+]+)\+/g,(match,key)=>{
213
- let v=vars[key];
214
- return v!== undefined ? v : match;
215
- }),
216
- async replace(name){
217
- const p=await this.load(name); // await короче Promise.then
218
- this.main=JSON.parse(this.parse(p)); // без замены языка нельзя начинать перевод
219
- for (let e of _.$.qa('[data-trans]')){
220
- let key=e.dataset.trans,
221
- text=this.main[key] || `<code>lang.get('${key}')</code>`,
222
- // ???: убрать обёртывание отсутсвующих ключей в <code />
223
- tag=e.tagName;
224
-
225
- if (tag=== 'IMG') e.src=text;
226
- else if (['INPUT','TEXTAREA'].includes(tag))
227
- e[e.type=== 'submit' ? 'value' :'placeholder']=text;
228
- else e.innerHTML=text;
229
- }
230
- // возвращаем для последующей обработки пакета, например для сохранения в _.storage
231
- return p;
232
- },
233
-
234
- /*
235
- * Получатели строки из пакета автоматически формируют HTML
236
- * Это позволяет заметно упростить работу с кодом
237
- * Вместо отдельного указания data-trans и lang.from
238
- * вы можете написать `<h1${_.lang.text('yourKey')}/h1>`
239
- * А пришлось бы писать `<h1 data-trans="yourKey">${_.lang.from('yourKey')}</h1>`
240
- * Согласитесь, и короче и удобнее ведь?
241
- * Не повторяйте моих ошибок и примите это как победу в лотерее
242
- *
243
- * !!!: если ключа в пакете нету, будет выброшен warning
244
- */
245
- attr:(i)=>` data-trans="${i}"`,
246
- from:i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
247
-
248
- text: i=>_.lang.attr(i)+`>${_.lang.from(i)}<`,
249
- submit: i=>_.lang.attr(i)+`value="${_.lang.from(i)}">`,
250
- /*
251
- * <input type=submit> работает во всех браузерах стабильно
252
- * не используйте на них обычный lang.input()
253
- * иначе у вас не отобразится текст
254
- */
255
- input: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}">`,
256
- textarea: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}"><`,
257
- img: i=>_.lang.attr(i)+`src="${_.lang.from(i)}"`,
258
- win(i){
259
- let text=this.from(i),
260
- dT=this.attr(i);
261
- if (text== null || text== ''){
262
- text=i;
263
- dT='';
264
- }
265
- return `${dT}>${text}<`;
266
- },
267
- },
268
- http:{
269
- /*
270
- * HTTP-КЛИЕНТ
271
- *
272
- * Обычная обёртка нав XHR для быстрых запросов
273
- * Использую XHR вместо fetch
274
- * Мне нужен прогресс загрузки (fetch его не даёт)
275
- * Да и вам тоже не помешает прогресс загрузки
276
- *
277
- * В defaultHeaders вы можете установить хедеры по умолчанию
278
- * Как пример Authorization: 'your token'
279
- * ???: добавить возможность игнорировать дефолтные хедеры
280
- */
281
- defaultHeaders:{},
282
- req(method,url,data='',headers={},fileProgressElement=false){
283
- return new Promise((resolve,reject)=>{
284
- let xhr=new XMLHttpRequest();
285
-
286
- xhr.open(method,url);
287
-
288
- let allHeaders={...this.defaultHeaders,...headers};
289
- for (let header in allHeaders)
290
- xhr.setRequestHeader(header,allHeaders[header]);
291
-
292
- // !!!: fileProgressElement ожидает <progress> элемент без min/max
293
- // Потому что value от 0 до 1
294
- if (fileProgressElement)
295
- xhr.upload.onprogress=(e)=>{
296
- if (e.lengthComputable){
297
- let percentage=(e.loaded / e.total);
298
- fileProgressElement.setAttribute('value',percentage);
299
- }
300
- };
301
-
302
- xhr.onreadystatechange=()=>{
303
- if (xhr.readyState=== 4)
304
- if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
305
- else reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
306
- };
307
- xhr.onerror=()=>reject(new Error('Network error'),xhr);
308
-
309
- xhr.send(data);
310
- });
311
- },
312
- },
313
- $:{
314
- D: document,
315
- id:(i)=>_.$.D.getElementById(i),
316
- q:(i,p=_.$.D)=>p.querySelector(i),
317
- qa:(i,p=_.$.D)=>p.querySelectorAll(i),
318
-
319
- on:(el,ev,fn,opts)=>el.addEventListener(ev,fn,opts),
320
- off:(el,ev,fn,opts)=>el.removeEventListener(ev,fn,opts),
321
-
322
- cliRect:e=>e.getBoundingClientRect(), // сокращение чтобы не писать 25+ символов
323
- },
324
- html(strs,...args){
325
- /*
326
- * Шаблонные строки в DOM
327
- *
328
- * Позволяет писать _.html`<div>${content}</div>`
329
- * И получать настоящий DOM-элемент, а не строку
330
- *
331
- * Почему через template?
332
- * - Скрипты не выполняются (никаких xss!)
333
- * - Можно создать несколько элементов разом
334
- * - Быстрее чем createElement для сложных структур
335
- * - Банально удобнее createElement для сложных древ
336
- */
337
- let strF=[];
338
- for (let i=0; i < args.length; i++)
339
- strF.push(strs[i],args[i]);
340
- strF.push(strs[strs.length - 1]);
341
- // убираем лишние пробелы, чтобы не плодить пустые текстовые ноды
342
- strF=strF.join('').trim().replace(/\s+/g,' ');
343
-
344
- const template=_.$.D.createElement('template');
345
- template.innerHTML=strF;
346
-
347
- const content=template.content;
348
- if (content.children.length=== 1)
349
- return content.firstChild;
350
- return content;
351
- },
352
- autoForm:{
353
- /*
354
- * АВТОСОХРАНЕНИЕ ФОРМ
355
- *
356
- * Позволяет сохранять состояние формы на случай
357
- * Если в офисе внезапно выключат свет
358
- *
359
- * Реализовывать удаление читателя событий я не стал
360
- * Зачем удалять обработчик если он вешается на форму а не на Document?
361
- */
362
- autoSave(form,delay=1000,cb){
363
- let t,
364
- save=()=>{
365
- let data={};
366
- new FormData(form).forEach((v,k)=>{
367
- if (data[k]!== undefined) {
368
- if (!Array.isArray(data[k])) data[k]=[data[k]];
369
- data[k].push(v);
370
- } else data[k]=v;
371
- });
372
- cb(data);
373
- };
374
- if(delay<1)return save();
375
- _.$.on(form,'input',()=>{
376
- clearTimeout(t);
377
- t=setTimeout(save,delay);
378
- });
379
- },
380
- write(form,data){
381
- Object.entries(data).forEach(([k,v])=>{
382
- let el=form.elements[k];
383
- if (!el) return;
384
- if (el.length) [...el].forEach((opt,i)=>
385
- /*
386
- * ???: переделать тернарники на внешные переменные для повышения читаемости
387
- *
388
- * Код ужасно читать, не отрицаю
389
- * Но причина сделать так проста - всего 2 строки с простыми условиями
390
- * Вместо 8 или 14 как у меня получалось ранее
391
- */
392
- opt[['checkbox','radio'].includes(opt.type) ? 'checked' : 'selected']=
393
- Array.isArray(v) ? v.includes(opt.value) : opt.value== v);
394
- else
395
- el.value=v;
396
- });
397
- return data;
398
- },
399
- },
400
- storage:class{
401
- constructor(strg,name){
402
- this._=strg;
403
- this.n=name;
404
- }
405
- get=(key)=>this._.getItem(this.n+key);
406
- set=(key,value)=>this._.setItem(this.n+key,value);
407
- remove=(key)=>this._.removeItem(this.n+key);
408
- clear=()=>Object.keys(this._)
409
- .filter(k=>k.startsWith(this.n))
410
- .forEach(k=>this._.removeItem(k));
411
- },
412
- hotkeys:{
413
- /*
414
- * ГОРЯЧИЕ КЛАВИШЫ
415
- *
416
- * Реализует самый настоящий press/release интерфейс
417
- * Если верить минификатору, после сжатия весит всего 790 байт
418
- *
419
- * В Object Hub уже есть текстовый редактор горячих клавиш
420
- * На базе этого движка, конечно давать textarea с js кодом...
421
- * Не самая безопасная затея, но как факт кастомизация широчайшая
422
- *
423
- * _holds работает не на массивах а на new Set()
424
- * Сеты работают намного быстрее при большом объёме данных
425
- * Вы же не хотите чтобы у вас тормозил поток с 100+ хоткеями
426
- * Из-за простого печатанья?
427
- */
428
- keys:{},
429
- _holds:new Set(),
430
- _:false,
431
-
432
- _parse:combo=>combo.split('+').map(k=>k.trim()),
433
- _match(keys) {
434
- // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
435
- for (let k of keys) if (!this._holds.has(k)) return false;
436
- return true;
437
- },
438
- _init() {
439
- if (this._) return;
440
- _.$.on(_.$.D,'keydown',e=>{
441
- this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
442
- // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
443
-
444
- for (let combo in this.keys) {
445
- let h=this.keys[combo];
446
- if (!this._match(h.keys)) continue;
447
-
448
- if (h.press && !h.active) {
449
- h.active=true; // active защищает от множественных срабатываний
450
- h.press(e);
451
- }
452
- }
453
- });
454
- _.$.on(_.$.D,'keyup',e=>{
455
- this._holds.delete(e.code);
456
-
457
- for (let combo in this.keys) {
458
- let h=this.keys[combo];
459
- if (h.active && !this._match(h.keys)) {
460
- h.active=false;
461
- h.release(e);
462
- }
463
- }
464
- });
465
- _.$.on(window,'blur',()=>{
466
- /*
467
- * При переключении в другое окно автоматического keyup не будет
468
- * Поэтому сбрасываем всё принудительно, мало ли
469
- */
470
- for (let combo in this.keys) {
471
- let h=this.keys[combo];
472
- if (h.active) {
473
- h.active=false;
474
- h.release();
475
- }
476
- }
477
- this._holds.clear();
478
- });
479
- this._=true;
480
- },
481
- on(combo,press,release) {
482
- this._init();
483
- let keys=this._parse(combo);
484
-
485
- this.keys[combo]={
486
- keys,
487
- // press/releace по умолчанию пустышки для сокращения синаксиса
488
- press: press || (()=>{}),
489
- release: release || (()=>{}),
490
- active: false
491
- };
492
-
493
- return this;
494
- },
495
- off(combo) {
496
- delete this.keys[combo];
497
- return this;
498
- },
499
- },
500
- win:{
501
- /*
502
- * МОДУЛЬ ОКОН
503
- * если вы спросите почему ньюхелпер я отвечу
504
- * winBox.js это 35 килобайт, здесь же вы получаете в 25 килобайт
505
- * И более широкий движок окон и документацию уровня...
506
- * А у кого нибуть вообще есть такие подробные документации в вебе?
507
- *
508
- * Реализует ограниченно-гибкий движок окон, функционал:
509
- * - открытие, разворот на весь экран, закрытие
510
- * - сворачивание в таскбар и разворчаивание
511
- * - нативный css-ресайз (resize:both)
512
- * - возможность двигать окна (работает на телефонах, я проверял)
513
- * - сохранение и загрузка окон по вашему выбору
514
- *
515
- * !!!: _opn() и toggleFull() могут сломать ваши окна!
516
- * Эти функции высчитывают координаты окна, и размер окна с учётом padding'а
517
- * Ни за что не вешайте на ваши окна transform:translate()!
518
- *
519
- * !!!: _opn() по умолчанию открывает окно по центру экрана
520
- * Если не идёт восстановление через write()
521
- * ???: стоит ли открывать окно в центре, или лучше дать "дефолтную функцию" позиционирования
522
- *
523
- * теперь мне надо вспомнить я рефакторил этот код 4 раза или 7 раз
524
- */
525
- manager:false,
526
- hider:false,
527
-
528
- winAttrs:'',
529
- dragAttrs:'',
530
- titleAttrs:'',
531
- renameAttrs:'',
532
- btnAttrs:'',
533
- hiderAttrs:'',
534
-
535
- defBtns:[
536
- ['–',w=>w.hide()],
537
- ['=',w=>w.toggleFull()],
538
- ['X',w=>w.close()],
539
- ],
540
-
541
- animOpen:'',
542
- animClose:'',
543
- animHide:'',
544
- animShow:'',
545
- animFullOn:'',
546
- animFullOff:'',
547
- _ae:{once:true},
548
-
549
- _ID(){
550
- let id;
551
- // Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
552
- // !!!: в теории можно задать любой айди
553
- // ???: проверить при скольки окнах генератор начинает тормозить
554
- do id=Math.random().toString(36).substring(2,8);
555
- while (_.wins[id]);
556
- return id;
557
- },
558
- _winBtn(win,text,func){
559
- let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
560
- _.$.on(b,'click',()=>func(win));
561
- return b;
562
- },
563
- _hiderBtn(win){
564
- let title=win.langs!== false ? _.lang.win('WINDOW-'+win.langs) : `>${win.name}<`,
565
- b=_.html`<button id=hider${win.id} ${this.hiderAttrs}${title}/button>`;
566
- _.$.on(b,'click',()=>this.show(win));
567
- return b;
568
- },
569
- _initWin(win){
570
- let D=_.$.D,
571
- wEl=win.elem,
572
- x1=0,y1=0,x2=0,y2=0,
573
- prevent=e=>e.preventDefault(),
574
- startW=e=>{
575
- let targ=e.target;
576
- // Проверяем куда нажали, если бы мы не проверяли,
577
- // То драггер не дал бы нам нажать на кнопки или изменить имя окна
578
- if (['BUTTON','INPUT'].includes(targ.tagName) || targ.closest('button,input')){
579
- return;
580
- }
581
- this.manager.appendChild(wEl);
582
-
583
- prevent(e);
584
- x2=e.clientX;
585
- y2=e.clientY;
586
-
587
- D.onpointermove=moveW;
588
-
589
- D.onpointerup=D.onpointercancel=stopW;
590
- },
591
- moveW=e=>{
592
- prevent(e);
593
- let cX=e.clientX,
594
- cY=e.clientY;
595
-
596
- x1=x2 - cX;
597
- y1=y2 - cY;
598
- x2=cX;
599
- y2=cY;
600
-
601
- wEl.style.top=(wEl.offsetTop - y1) + "px";
602
- wEl.style.left=(wEl.offsetLeft - x1) + "px";
603
- },
604
- stopW=()=>['move','up','cancel'].map(e=>D['onpointer'+e]=null),
605
- dr=win.drag;
606
- dr.onpointerdown=startW;
607
- dr.ontouchmove=prevent;
608
- },
609
- open(name,content='',customAttrs=''){
610
- let winId=this._ID(),
611
- winState=_.wins[winId]={
612
- id:winId,
613
- name:name,
614
- langs:name,
615
- state:'opened',
616
- full:false,
617
- inRename:false,
618
- // Если окно новое, координаты полностью нулевые,
619
- // Нужно чтобы проверять создаётся ли окно и если да то задавать координаты
620
- onUnfull:{top:0,left:0,width:0,height:0},
621
- attrs:customAttrs,
622
- elem:false,
623
- drag:false,
624
- content:false,
625
- };
626
- return this._opn(winState,content);
627
- },
628
- _opn(w,content=''){
629
- if (!this.manager || !this.hider) throw new Error('Window managers not inited');
630
-
631
- let wId=w.id,
632
- html=
633
- _.html`<div id=${wId} ${this.winAttrs} ${w.attrs}>
634
- <div style="display:flex;justify-content:space-between;align-items:center"
635
- ${this.dragAttrs} id=DRAGGER${wId}>
636
- <span ${this.titleAttrs} id=title${wId}${_.lang.win('WINDOW-'+w.name)}/span>
637
- <div id=btns${wId}></div>
638
- </div>
639
- <div id=content${wId} style=overflow:auto;width:100%;height:100%>
640
- ${content.replace(/\{winId\}/g,wId)}
641
- </div>
642
- </div>`,
643
- btns=_.$.q(`#btns${wId}`,html);
644
- for(let b of this.defBtns) btns.append(this._winBtn(w,...b));
645
- html.style.overflow='hidden';
646
- html.style.resize='both';
647
-
648
- let anim=this.animOpen;
649
- if (anim)
650
- _.$.on(html,'animationend',()=>html.classList.remove(anim),this._ae);
651
- w.setTitle=nT=>_.win.setTitle(w,nT);
652
- w.toggleFull=e=>_.win.toggleFull(w);
653
- w.close=e=>_.win.close(w);
654
- w.hide=e=>_.win.hide(w);
655
- w.show=e=>_.win.show(w);
656
- this.manager.append(html);
657
-
658
- let win=w.elem=_.$.id(wId),
659
- c=_.$.cliRect(_.$.id('content'+wId)),r=_.$.cliRect(win),
660
- padX=r.width - c.width,padY=r.height - c.height;
661
- w.drag=_.$.id('DRAGGER'+wId);
662
- w.content=_.$.id('content'+wId);
663
-
664
- if (w.onUnfull.width === 0) {
665
- // Здесь и задаются координаты...
666
- // Мастера клин кода не выносите мне мозги прошу
667
- // Оно же работает!!!
668
- if (!w.attrs.includes('top')) {
669
- win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
670
- win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
671
- }
672
- if (!w.attrs.includes('width')) win.style.height=(win.offsetHeight - padX) + 'px';
673
- if (!w.attrs.includes('height')) win.style.width=(win.offsetWidth - padY) + 'px';
674
- } else
675
- for (let pos in w.onUnfull)
676
- win.style[pos] = w.onUnfull[pos] + 'px'
677
-
678
- this._initWin(w);
679
- _.$.on(w.drag,'contextmenu',(e)=>{
680
- e.preventDefault();
681
- if(e.target.closest('button')) return;
682
- let wT=_.$.id('title'+wId);
683
- if (!w.inRename){
684
- wT.innerHTML=`<input ${this.renameAttrs} id=rename${wId} value="${wT.textContent}">`;
685
- w.inRename=true;
686
- }else{
687
- this.setTitle(w,_.$.id('rename'+wId).value);
688
- w.inRename=false;
689
- }
690
- });
691
-
692
- if (w.state === 'hidened') w.hide();
693
-
694
- return w;
695
- },
696
- setTitle(win,newT){
697
- win.langs=false;
698
- win.name=newT;
699
- let t=_.$.id('title'+win.id),
700
- h=_.$.id('hider'+win.id);
701
- t.innerHTML=newT;
702
- t.removeAttribute('data-trans');
703
- if (h){
704
- h.innerHTML=newT;
705
- h.removeAttribute('data-trans');
706
- }
707
- },
708
- toggleFull(win){
709
- let wEl=win.elem,
710
- ws=wEl.style,
711
- wc=wEl.classList,
712
- cont=_.$.cliRect(_.$.id('content'+win.id)),
713
- rect=_.$.cliRect(wEl),
714
- padX=rect.width - cont.width,
715
- padY=rect.height - cont.height,
716
- aOn=this.animFullOn,
717
- aOff=this.animFullOff,
718
- fd={
719
- top: rect.top, left: rect.left,
720
- width: cont.width, height: cont.height,
721
- },
722
- unful=()=>{
723
- ws.top=old.top + 'px';
724
- ws.left=old.left + 'px';
725
- ws.width=old.width + 'px';
726
- ws.height=old.height + 'px';
727
- },
728
- doFul=()=>{
729
- if (aOn) wc.remove(aOn);
730
- win.full=true;
731
- win.onUnfull=fd;
732
- ws.top=0;
733
- ws.left=0;
734
- ws.width=`calc(100% - ${padX}px)`;
735
- ws.height=`calc(100% - ${padY}px)`;
736
- win.drag.onpointerdown=null;
737
- },
738
- doUnful=()=>{
739
- if (aOff) wc.remove(aOff);
740
- unful();
741
- win.full=false;
742
- this._initWin(win);
743
- },
744
- old=win.onUnfull;
745
- if (!win.full) {
746
- if (aOn) {
747
- wc.add(aOn);
748
- _.$.on(wEl,'animationend',doFul,this._ae);
749
- }else doFul();
750
- } else {
751
- if (aOff) {
752
- wc.add(aOff);
753
- unful();
754
- _.$.on(wEl,'animationend',doUnful,this._ae);
755
- }else doUnful();
756
- }
757
- },
758
- close(win){
759
- let w=win.elem,
760
- remover=()=>{
761
- let dr=win.drag,D=_.$.D;
762
- dr.onpointerdown=dr.ontouchmove=null;
763
- // Удаляем обработчики висящие на документе
764
- // Если их не удалять рано или поздно случится утечка памяти
765
- // Я не знаю как я жил во времена 2.0 когда движок только появился
766
- ['move','up','cancel'].map(e=>D['onpointer'+e]=null);
767
- w.remove();
768
- delete _.wins[win.id];
769
- };
770
- if (w.style.display== 'none'){
771
- _.$.id('hider'+win.id).remove();
772
- remover();
773
- }else{
774
- let anim=this.animClose;
775
- if(anim){
776
- w.classList.add(anim);
777
- _.$.on(w,'animationend',remover,this._ae);
778
- }else
779
- remover();
780
- }
781
- },
782
- hide(win){
783
- let wEl=win.elem,
784
- wc=wEl.classList,
785
- anim=this.animHide,
786
- hider=()=>{
787
- wEl.style.display='none';
788
- if(anim)wc.remove(anim);
789
- win.state='hidened';
790
- this.hider.append(this._hiderBtn(win));
791
- }
792
- if(anim){
793
- wc.add(anim);
794
- _.$.on(wEl,'animationend',hider,this._ae);
795
- }else
796
- hider();
797
- },
798
- show(win){
799
- let wEl=win.elem,
800
- wc=wEl.classList,
801
- anim=this.animShow,
802
- hider=_.$.id('hider'+win.id),
803
- shower=()=>{
804
- if(anim)wc.remove(anim);
805
- win.state='opened';
806
- }
807
- wEl.style.display='';
808
- hider.remove();
809
- if(anim){
810
- wc.add(anim);
811
- _.$.on(wEl,'animationend',shower,this._ae);
812
- }else
813
- shower()
814
- },
815
- read(){
816
- let store = {};
817
- for (let winId in _.wins) {
818
- let win={..._.wins[winId]},
819
- size=win.onUnfull,
820
- wEl = win.elem,
821
- c=_.$.cliRect(win.content),r=_.$.cliRect(wEl);
822
- win.realContent=win.content.innerHTML;
823
- size.top=r.top;
824
- size.left=r.left;
825
- size.height=wEl.offsetHeight - (r.height - c.height);
826
- size.width=wEl.offsetWidth - (r.width - c.width);
827
- delete win.elem;
828
- delete win.drag;
829
- delete win.content;
830
- store[winId] = win;
831
- }
832
- return store;
833
- },
834
- write(state){
835
- for (let winId in state) {
836
- let win=state[winId],
837
- content=win.realContent;
838
- delete win.realContent;
839
- _.wins[winId] = win;
840
- this._opn(win,content);
841
- }
842
- },
843
- },
844
- wins:{},
845
- };
846
-
847
- _.$.on(window,'popstate',()=>{
848
- let l=_.link
849
- /*
850
- * popstate срабатывает когда:
851
- * - пользователь прыгает по истории назад/вперёд
852
- * - мы вызываем history.pushState (не replaceState)
853
- *
854
- * _i различает эти случаи:
855
- * true = пользователь прыгнул назад
856
- * false = страница пишет свой адрес в ссылку
857
- */
858
- // ???: некоторые браузеры могут вызывать popstate и при реплейсе
859
- if (!l._i) {
860
- // здесь происходит перенос команд при popstate
861
- // читайте _.lang.get() если хотите узнать почему
862
- let nUrl='?' + [l.compile()[0],...l._cmd].join('&');
863
- l._i=true;
864
- history.replaceState(null,null,nUrl);
865
- l.get();
866
- } else
867
- l._i=false;
868
- });
869
-
870
- return _}();
1
+ /*
2
+ * Перед вами код newHelper.js версии 2.1.4, он построен на базе фабрики
3
+ * Которая начинается с Intl.newHelper=function(){...};
4
+ * Причина использоватся Intl.newHelper банально проста
5
+ * Если я сейчас засираю глобалскоуп одной полу гибкой переменной
6
+ * То почему бы не начать отказываться от засирания глобал скоупа как такового
7
+ * И да, для инициализации ньюхелпера реально нужно писать
8
+ * yourVariable = Intl.newHelper()
9
+ *
10
+ * Стиль комментариев
11
+ * FIXME - странное поведение функции, которое желательно бы переделать
12
+ * ??? - требует уточнения
13
+ * !!! - обратите внимание
14
+ *
15
+ * ???: рассмотреть переход на es6 экспорт вместо вкладывания фабрики в Intl
16
+ *
17
+ * ???: Последний глобальный эвент лисенер можно вынести внутрь _.link.get()
18
+ *
19
+ * Новые модули, готовятсяк релизу в 2.2
20
+ * их апи может быть чуть чуть нестабильно:
21
+ * form
22
+ * tables
23
+ * drag (портирован из окон)
24
+ * pipe/pipeAsync
25
+ */
26
+
27
+ Intl.newHelper=function() {let _ = {
28
+ link: {
29
+ /*
30
+ * МОДУЛЬ ССЫЛОК
31
+ *
32
+ * Работает по принципу [ссылка, команды...]
33
+ * Пример: ?page=home&debug&lang=ru
34
+ * ^^^^^^^^ ^^^^^ ^^^^^^
35
+ * страница команды
36
+ *
37
+ * В процессе разработки ядра 2.0 в Object hub я понял
38
+ * Что команды могут быть очень полезными для отладки
39
+ * Но в теории на них можно повешать все модальные и прочие действия
40
+ *
41
+ * !!!: в функции get() работает весь роутинг, в т.ч. вложенный для страниц
42
+ */
43
+ basePage: ()=>{},
44
+ defTitle: '',
45
+ actions: {},
46
+ commands: {},
47
+
48
+ _i: true, // _i - блокировщик pushState в set()
49
+ _cmd: [],
50
+ compile: ()=>location.search.replace('?','').split('&'),
51
+ set(page, title = this.defTitle) {
52
+ if (title) document.title = title;
53
+ if (!this._i) {
54
+ let link = this.compile();
55
+ link[0] = page;
56
+ history.pushState(null,null,'?'+link.join('&'));
57
+ }
58
+ this._i = false;
59
+ },
60
+ add(cmd) {
61
+ let link = this.compile();
62
+ if (!link.includes(cmd)) {
63
+ link.push(cmd);
64
+ this._cmd.push(cmd);
65
+ history.replaceState(null,null,'?'+link.join('&'));
66
+ }
67
+ },
68
+ remove(cmd) {
69
+ let link = this.compile();
70
+ if (link.includes(cmd)){
71
+ let c = this._cmd;
72
+ link.splice(link.indexOf(cmd),1);
73
+ c.splice(c.indexOf(cmd),1);
74
+ history.replaceState(null,null,'?'+link.join('&'));
75
+ }
76
+ },
77
+ get() {
78
+ /*
79
+ * Страницы бросают ошибку чтобы вызвать базовую страницу
80
+ * Команды тем временем так не делают
81
+ * Потому что сломанная команда не так страшна как сломанная страница
82
+ *
83
+ * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
84
+ * Сделано это для переноса команд при прыжках по истории
85
+ */
86
+ let links = this.compile(),
87
+ [ firstKey, fisrtValue ] = links[0].split('='),
88
+ cmds = links.slice(1);
89
+ try {
90
+ let dirs = firstKey.split('/'),
91
+ dir = this.actions,
92
+ main = dir[firstKey];
93
+ if (!firstKey.includes('/')) {
94
+ main(fisrtValue);
95
+ } else {
96
+ for (let p of dirs){
97
+ let kDir = dir[p+'/'];
98
+ if (kDir)
99
+ dir = kDir;
100
+ else{
101
+ dir[p](fisrtValue);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ } catch (e) {
107
+ this.basePage();
108
+ throw e;
109
+ }
110
+ this._cmd = cmds;
111
+ cmds.forEach(cmdPre => {
112
+ let [ key, value ] = cmdPre.split('=');
113
+ let cmd = this.commands[key];
114
+ if (cmd)
115
+ cmd(value);
116
+ else
117
+ console.error(new Error(`command '${cmd}' doesn't exist!`))
118
+ });
119
+ },
120
+ },
121
+ lazy: {
122
+ /*
123
+ * МОДУЛЬ ЛЕНИ
124
+ *
125
+ * Создаёт в глобальной области видимости прокси функции
126
+ * Вызывающие загрузку скрипта с внещним модулем
127
+ * Был сделан через глобальную область, так намного проще создавать лень
128
+ *
129
+ * !!!: Функции обёртки в register() должны быть повешаны на window
130
+ * Иначе lazy._ провалится в рекурсию ошибок, не наступайте на мои грабли
131
+ *
132
+ * ???: будет ли легче создавать лень в легаси проектах через es6 импорты
133
+ */
134
+ loaded: {},
135
+ load(url, ...args) {
136
+ /*
137
+ * ...args передаются в Promise.resolve(args)
138
+ * Это позволяет делать _.lazy.load('script.js', 'данные', 'для', 'колбека')
139
+ * И потом в .then((a,b,c)=>...) получать эти аргументы
140
+ *
141
+ * Тройное состояние скрипта в lazy.loaded:
142
+ * - true: уже загружен => сразу резолвим
143
+ * - Promise: грузится сейчас => ждём тот же промис
144
+ * - undefined: ещё не грузили => создаём новый промис
145
+ *
146
+ * Это защита от двойной загрузки одного скрипта
147
+ */
148
+ let key = url.split('?')[0], // отсекаем параметры, чтобы не дублировать
149
+ state = this.loaded;
150
+ if (state[key] === true)
151
+ return Promise.resolve(args);
152
+ if (state[key] instanceof Promise)
153
+ return state[key].then(()=>args);
154
+
155
+ let promise = new Promise((resolve,reject)=>{
156
+ let scr = document.createElement('script');
157
+ scr.src = url;
158
+ scr.onload = ()=>{
159
+ state[key] = true;
160
+ resolve(args);
161
+ };
162
+ scr.onerror = ()=>{
163
+ delete state[key];
164
+ reject(new Error('Failed to load '+url));
165
+ };
166
+ document.head.append(scr);
167
+ });
168
+ state[key] = promise;
169
+ return promise;
170
+ },
171
+ register(script, funcs) {
172
+ if (!Array.isArray(funcs))
173
+ return new Error('Array required for register');
174
+
175
+ for (let fn of funcs) {
176
+ let fns = fn.split('.'),
177
+ method = fns.pop(),
178
+ path = fns.slice(0,-1);
179
+ console.log(path)
180
+ window[fn] = (...a)=>
181
+ this.lazy(script,fn).then(f=>f(...a));
182
+ }
183
+ },
184
+ async lazy(scr, fn) {
185
+ let w = window,
186
+ wrapper = w[fn];
187
+
188
+ await this.load(scr); // await короче Promise.then
189
+
190
+ if (wrapper !== w[fn])
191
+ return w[fn];
192
+ throw new Error(`Function ${fn} not loaded from ${scr}`);
193
+ },
194
+ },
195
+ lang: {
196
+ /*
197
+ * МОДУЛЬ ПЕРЕВОДОВ
198
+ *
199
+ * По слухам этот модуль лучше чем многие i18n реализации
200
+ * Всё потому что он из коробки умеет переводить страницу без перезагрузки
201
+ *
202
+ * !!!: parse() обрабатывает ключи из vars и подставляет их значения
203
+ * ваш +ключ+ становится значением, и это значение динамичное
204
+ * Так удобнее отображать динамичные данные на сайтах
205
+ * Например никнейм пользователя
206
+ */
207
+ addr: '',
208
+ vars: {},
209
+ main: {},
210
+
211
+ load: name => fetch(_.lang.addr + name + '.json'),
212
+ parse: (packet, vars = _.lang.vars)=>
213
+ // ???: переделать под общий синтаксис типа {var}
214
+ packet.replace(/\+([^+]+)\+/g, (match, key)=>{
215
+ let v = vars[key];
216
+ return v !== undefined ? v : match;
217
+ }),
218
+ async replace(name){
219
+ const packet = await this.load(name);
220
+ this.main = JSON.parse(this.parse(packet)); // без замены языка нельзя начинать перевод
221
+
222
+ for (let el of document.querySelectorAll('[data-trans]')) {
223
+ let key = el.dataset.trans,
224
+ text = this.main[key] || key,
225
+ tag = el.tagName;
226
+
227
+ if (tag === 'IMG')
228
+ el.src = text;
229
+ else if (['INPUT','TEXTAREA'].includes(tag))
230
+ el[ el.type === 'submit' ? 'value' : 'placeholder' ] = text;
231
+ else
232
+ el.innerHTML = text;
233
+ }
234
+ // возвращаем для последующей обработки пакета, например для сохранения в _.storage
235
+ return packet;
236
+ },
237
+
238
+ /*
239
+ * Получатели строки из пакета автоматически формируют HTML
240
+ * Это позволяет заметно упростить работу с кодом
241
+ * Вместо отдельного указания data-trans и lang.from
242
+ * вы можете написать `<h1${_.lang.text('yourKey')}/h1>`
243
+ * А пришлось бы писать `<h1 data-trans="yourKey">${_.lang.from('yourKey')}</h1>`
244
+ * Согласитесь, и короче и удобнее ведь?
245
+ * Не повторяйте моих ошибок и примите это как победу в лотерее
246
+ *
247
+ * !!!: если ключа в пакете нету, будет выброшен warning
248
+ */
249
+ attr: i=>` data-trans="${i}"`,
250
+ from: i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
251
+
252
+ text: i=>_.lang.attr(i)+`>${_.lang.from(i)}<`,
253
+ submit: i=>_.lang.attr(i)+`value="${_.lang.from(i)}">`, // <input type=submit>
254
+ input: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}">`,
255
+ textarea: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}"><`,
256
+ img: i=>_.lang.attr(i)+`src="${_.lang.from(i)}"`,
257
+ winTitle(i) {
258
+ let text = this.from(i),
259
+ dataTrans = this.attr(i);
260
+ if (text == null || text == '') {
261
+ text = i;
262
+ dataTrans = '';
263
+ }
264
+ return `${dataTrans}>${text}<`;
265
+ },
266
+ },
267
+ http: {
268
+ /*
269
+ * HTTP-КЛИЕНТ
270
+ *
271
+ * Обычная обёртка нав XHR для быстрых запросов
272
+ * Использую XHR вместо fetch
273
+ * Мне нужен прогресс загрузки (fetch его не даёт)
274
+ * Да и вам тоже не помешает прогресс загрузки
275
+ *
276
+ * В defaultHeaders вы можете установить хедеры по умолчанию
277
+ * Как пример Authorization: 'your token'
278
+ * ???: добавить возможность игнорировать дефолтные хедеры
279
+ */
280
+ defaultHeaders: {},
281
+ req(method, url, data = '', headers = {}, fileProgressElement = false) {
282
+ return new Promise((resolve, reject)=>{
283
+ let xhr = new XMLHttpRequest();
284
+
285
+ xhr.open(method, url);
286
+
287
+ let allHeaders = { ...this.defaultHeaders, ...headers };
288
+ for (let header in allHeaders)
289
+ xhr.setRequestHeader(header, allHeaders[header]);
290
+
291
+ // !!!: fileProgressElement ожидает <progress> элемент без min/max
292
+ // Потому что value от 0 до 1
293
+ if (fileProgressElement)
294
+ xhr.upload.onprogress= e=>{
295
+ if (e.lengthComputable) {
296
+ let percentage = (e.loaded / e.total);
297
+ fileProgressElement.setAttribute('value', percentage);
298
+ }
299
+ };
300
+
301
+ xhr.onreadystatechange= ()=>{
302
+ if (xhr.readyState=== 4)
303
+ if (xhr.status >= 200 && xhr.status < 300)
304
+ resolve(xhr.response);
305
+ else
306
+ reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
307
+ };
308
+ xhr.onerror = ()=>
309
+ reject(new Error('Network error'), xhr);
310
+
311
+ xhr.send(data);
312
+ });
313
+ },
314
+ get: (url, headers={})=>
315
+ _.http.req('GET', url, false, headers),
316
+ post: (url, data = '', headers = {}, fileProgressElement = false)=>
317
+ _.http.req('POST', url, data, headers, fileProgressElement)
318
+ },
319
+ html(strs, ...args) {
320
+ /*
321
+ * Шаблонные строки в DOM
322
+ *
323
+ * Позволяет писать _.html`<div>${content}</div>`
324
+ * И получать настоящий DOM-элемент, а не строку
325
+ *
326
+ * Почему через template?
327
+ * - Скрипты не выполняются (никаких xss!)
328
+ * - Можно создать несколько элементов разом
329
+ * - Быстрее чем createElement для сложных структур
330
+ * - Банально удобнее createElement для сложных древ
331
+ */
332
+ let fullStr = '',
333
+ DOMs = [];
334
+ for (let i=0; i < args.length; i++) {
335
+ fullStr += strs[i];
336
+ let arg = args[i];
337
+ if (arg && arg.nodeType) {
338
+ fullStr += `<!--${DOMs.length}-->`;
339
+ DOMs.push(arg);
340
+ } else {
341
+ fullStr += arg;
342
+ }
343
+ }
344
+ fullStr += strs[strs.length - 1];
345
+
346
+ const template = document.createElement('template');
347
+ template.innerHTML = fullStr;
348
+ const content = template.content;
349
+
350
+ // для создания вложенности html элементов заменяем плейсхолдеры <!--${DOMs.length}-->
351
+ const it = document.createTreeWalker(
352
+ content,
353
+ NodeFilter.SHOW_COMMENT
354
+ );
355
+ let node, i = 0;
356
+ for (; node = it.nextNode(); )
357
+ node.replaceWith(DOMs[i++]);
358
+
359
+ if (content.children.length === 1)
360
+ return content.firstChild;
361
+ return content;
362
+ },
363
+ pipe(data, ...fns) {
364
+ /*
365
+ * КАСТОМНЫЙ PIPE ОПЕРАТОР
366
+ *
367
+ * Никакой магии, обычный синхронный |>
368
+ * для мутации таблиц будет самое то
369
+ */
370
+ for (const fn of fns)
371
+ data = fn(data);
372
+ return data;
373
+ },
374
+ async pipeAsync(data, ...fns) {
375
+ /*
376
+ * КАСТОМНЫЙ PIPE ОПЕРАТОР 2
377
+ *
378
+ * Никакой магии, обычный асинхронный |>
379
+ * для получения и мутации данных сойдёт
380
+ */
381
+ for (const fn of fns) {
382
+ let waiter = await data;
383
+ data = await fn(waiter);
384
+ }
385
+ return data;
386
+ },
387
+ form: {
388
+ /*
389
+ * АВТОСОХРАНЕНИЕ ФОРМ
390
+ *
391
+ * Позволяет сохранять состояние формы на случай
392
+ * Если в офисе внезапно выключат свет
393
+ *
394
+ * ???: может сделать более полноценный модуль форм
395
+ * с встроенной валидацией, или чем нибуть ещё
396
+ */
397
+ read(form) {
398
+ let data = {};
399
+ new FormData(form).forEach((value, key)=>{
400
+ if (data[key] !== undefined) {
401
+ if (!Array.isArray(data[key]))
402
+ data[key] = [data[key]];
403
+ else
404
+ data[key].push(value);
405
+ } else
406
+ data[key] = value;
407
+ });
408
+ return data;
409
+ },
410
+ write(form, data) {
411
+ Object.entries(data).forEach(([key,value])=>{
412
+ let el = form.elements[key];
413
+ if (!el)
414
+ return;
415
+ if (el.length)
416
+ [...el].forEach((opt,i)=>{
417
+ let isCheckBox = 'selected';
418
+ if (['checkbox','radio'].includes(opt.type))
419
+ isCheckBox = 'checked';
420
+
421
+ let select = false;
422
+ if (Array.isArray(value)) {
423
+ if (value.includes(opt.value))
424
+ select = true;
425
+ } else if (opt.value == value)
426
+ select = true;
427
+
428
+ opt[isCheckBox] = select;
429
+ });
430
+ else
431
+ el.value = value;
432
+ });
433
+ return data;
434
+ },
435
+ },
436
+ tables(name, columns, raw, rowKey = 'ID', selection = false) {
437
+ /*
438
+ * МОДУЛЬ АВТОТАБЛИЦ
439
+ *
440
+ * Позволяет быстро генерировать таблицы с особыми свойствами
441
+ *
442
+ * !!!: в columns параметр mutate работает как парсер значения
443
+ * !!!: сортируйте сами путём мутации data, javascript как никак умеет
444
+ * или вообще сортируйте на сервере
445
+ *
446
+ * ???: рассмотреть переделку апи т.к. в текущей реализации гибкость слишком низкая
447
+ */
448
+ if (!Array.isArray(raw))
449
+ raw = Object.values(raw);
450
+ let state = {
451
+ name: name,
452
+ columns: columns,
453
+ raw: raw,
454
+ rowKey: rowKey,
455
+ data: raw,
456
+ selected: new Set(),
457
+ elem: null,
458
+
459
+ build(elem) {
460
+ this.elem = elem;
461
+ this.render();
462
+ return this;
463
+ },
464
+
465
+ getSelected() {
466
+ return [ ...this.selected ];
467
+ },
468
+
469
+ render() {
470
+ if (!this.elem) return;
471
+ let html = '';
472
+ html += `<thead><tr>`;
473
+ if (selection)
474
+ html += `<th></th>`;
475
+ for (let c of this.columns)
476
+ html += `<th>${c.title || c.key}</th>`;
477
+ html += `</tr></thead>`;
478
+
479
+ html += `<tbody>`;
480
+ for (let row of this.data) {
481
+ let id = row[this.rowKey];
482
+ let sel = this.selected.has(id) ? `selected` : ``;
483
+
484
+ html += `<tr data-id="${id}" class="${sel}">`;
485
+ if (selection)
486
+ html +=
487
+ `<td><input type=checkbox name="${this.name}" value="${id}" /></td>`;
488
+ for (let c of this.columns) {
489
+ let v = row[c.key];
490
+ if (c.mutate)
491
+ v = c.mutate(row);
492
+ html += `<td>${v ?? ``}</td>`;
493
+ }
494
+ html += `</tr>`;
495
+ }
496
+ html += `</tbody>`;
497
+
498
+ this.elem.innerHTML = `<table>${html}</table>`;
499
+ this.elem.firstChild.addEventListener('change', e => {
500
+ let targ = e.target;
501
+ if (targ.type !== 'checkbox' || targ.name !== this.name)
502
+ return;
503
+
504
+ let id = targ.value;
505
+
506
+ if (targ.checked)
507
+ this.selected.add(id);
508
+ else
509
+ this.selected.delete(id);
510
+ })
511
+ }
512
+ };
513
+ return state;
514
+ },
515
+ storage: class {
516
+ constructor(storage, name) {
517
+ this._ = storage;
518
+ this.n = name;
519
+ }
520
+ get = key=> this._.getItem(this.n + key);
521
+ set = (key, value)=>this._.setItem(this.n + key, value);
522
+ remove = key=> this._.removeItem(this.n + key);
523
+ clear = ()=>Object.keys(this._)
524
+ .filter(k => k.startsWith(this.n))
525
+ .forEach(k => this._.removeItem(k));
526
+ },
527
+ err: {
528
+ init() {
529
+ window.addEventListener('error',_.err.handleGlobal);
530
+ window.addEventListener('unhandledrejection',_.err.handleRejection);
531
+ },
532
+ print: ()=>{},
533
+
534
+ errors: {},
535
+ _c: 0,
536
+ log(err) {
537
+ _.err.print(_.err._c,err);
538
+ _.err._c++;
539
+ _.err.errors[_.err._c]=err;
540
+ },
541
+ handleGlobal(message,source,line,column,error){
542
+ console.error(message,source+':'+line+':'+column,error)
543
+ _.err.log(message + `\n IN ${source} ON LINE ${line} IN COLUMN ${column}`);
544
+ },
545
+ handleRejection(e){
546
+ const err = e.reason || e;
547
+ console.error(err);
548
+ _.err.log(
549
+ `PROMISE ERROR\n`+
550
+ `${e.stack || e}`
551
+ );
552
+ },
553
+ },
554
+ hotkeys: {
555
+ /*
556
+ * ГОРЯЧИЕ КЛАВИШЫ
557
+ *
558
+ * Реализует самый настоящий press/release интерфейс
559
+ * Если верить минификатору, после сжатия весит всего 790 байт
560
+ *
561
+ * В Object Hub уже есть текстовый редактор горячих клавиш
562
+ * На базе этого движка, конечно давать textarea с js кодом...
563
+ * Не самая безопасная затея, но как факт кастомизация широчайшая
564
+ *
565
+ * _holds работает не на массивах а на new Set()
566
+ * Сеты работают намного быстрее при большом объёме данных
567
+ * Вы же не хотите чтобы у вас тормозил поток с 100+ хоткеями
568
+ * Из-за простого печатанья?
569
+ */
570
+ keys: new Map(),
571
+ _holds: new Set(),
572
+ _: false,
573
+
574
+ _parse: combo => combo.split('+').map(k=>k.trim()),
575
+ _match(keys) {
576
+ // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
577
+ for (let k of keys) if (!this._holds.has(k)) return false;
578
+ return true;
579
+ },
580
+ _init() {
581
+ if (this._)
582
+ return;
583
+ document.addEventListener('keydown', e=>{
584
+ this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
585
+ // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
586
+
587
+ for (let hotkey of this.keys.values()) {
588
+ if (!this._match(hotkey.keys))
589
+ continue;
590
+ if (hotkey.press && !hotkey.active) {
591
+ hotkey.active = true; // active защищает от множественных срабатываний
592
+ hotkey.press(e);
593
+ }
594
+ }
595
+ });
596
+ document.addEventListener('keyup', e=>{
597
+ this._holds.delete(e.code);
598
+
599
+ for (let hotkey of this.keys.values()) {
600
+ if (hotkey.active && !this._match(hotkey.keys)) {
601
+ hotkey.active=false;
602
+ hotkey.release(e);
603
+ }
604
+ }
605
+ });
606
+ window.addEventListener('blur', e=>{
607
+ /*
608
+ * При переключении в другое окно автоматического keyup не будет
609
+ * Поэтому сбрасываем всё принудительно, мало ли
610
+ */
611
+ for (let hotkey of this.keys.values()) {
612
+ if (hotkey.active) {
613
+ hotkey.active = false;
614
+ hotkey.release();
615
+ }
616
+ }
617
+ this._holds.clear();
618
+ });
619
+ this._=true;
620
+ },
621
+ on(combo, press, release) {
622
+ this._init();
623
+ let keys = this._parse(combo);
624
+
625
+ this.keys.set(combo, {
626
+ keys,
627
+ // press/releace по умолчанию пустышки для сокращения синаксиса
628
+ press: press || (()=>{}),
629
+ release: release || (()=>{}),
630
+ active: false
631
+ });
632
+
633
+ return this;
634
+ },
635
+ off(combo) {
636
+ this.keys.delete(combo);
637
+ return this;
638
+ },
639
+ },
640
+ drag: {
641
+ _i: false,
642
+ active: new Map(),
643
+ prevent: e=>e.preventDefault(),
644
+ init(dragger, mover, onStart, onStop) {
645
+ let start=e=>{
646
+ // Проверяем куда нажали, если бы мы не проверяли,
647
+ // То драггер не дал бы нам нажать на кнопки или изменить имя окна
648
+ if (e.target.closest('button,input')) return;
649
+
650
+ this.prevent(e);
651
+
652
+ this.active.set(e.pointerId,{
653
+ x:e.clientX,
654
+ y:e.clientY,
655
+ mover:mover,
656
+ onStop:onStop
657
+ });
658
+
659
+ onStart?.(e);
660
+ };
661
+ if (!this._i) {
662
+ document.addEventListener("pointermove", (e) => this.move(e));
663
+ document.addEventListener("pointerup", (e) => this.stop(e));
664
+ document.addEventListener("pointercancel", (e) => this.stop(e));
665
+ this._i = true;
666
+ }
667
+ dragger.onpointerdown=start;
668
+ dragger.ontouchmove=this.prevent;
669
+ },
670
+ move(e) {
671
+ let p=this.active.get(e.pointerId);
672
+ if(!p) return;
673
+ this.prevent(e);
674
+
675
+ let dx=p.x - e.clientX,
676
+ dy=p.y - e.clientY;
677
+
678
+ p.x=e.clientX;
679
+ p.y=e.clientY;
680
+
681
+ let mov = p.mover;
682
+ mov.style.top=(mov.offsetTop - dy)+"px";
683
+ mov.style.left=(mov.offsetLeft - dx)+"px";
684
+ },
685
+ stop(e) {
686
+ this.active.get(e.pointerId)?.onStop?.(e);
687
+ this.active.delete(e.pointerId);
688
+ },
689
+ },
690
+ win:{
691
+ /*
692
+ * МОДУЛЬ ОКОН
693
+ * если вы спросите почему ньюхелпер я отвечу
694
+ * winBox.js это 35 килобайт, здесь же вы получаете в 25 килобайт
695
+ * И более широкий движок окон и документацию уровня...
696
+ * А у кого нибуть вообще есть такие подробные документации в вебе?
697
+ *
698
+ * Реализует ограниченно-гибкий движок окон, функционал:
699
+ * - открытие, разворот на весь экран, закрытие
700
+ * - сворачивание в таскбар и разворчаивание
701
+ * - нативный css-ресайз (resize:both)
702
+ * - возможность двигать окна (работает на телефонах, я проверял)
703
+ * - сохранение и загрузка окон по вашему выбору
704
+ *
705
+ * !!!: _opn() и toggleFull() могут сломать ваши окна!
706
+ * Эти функции высчитывают координаты окна, и размер окна с учётом padding'а
707
+ * Ни за что не вешайте на ваши окна transform:translate()!
708
+ *
709
+ * !!!: _opn() по умолчанию открывает окно по центру экрана
710
+ * Если не идёт восстановление через write()
711
+ * ???: стоит ли открывать окно в центре, или лучше дать "дефолтную функцию" позиционирования
712
+ *
713
+ * теперь мне надо вспомнить я рефакторил этот код 4 раза или 7 раз
714
+ */
715
+ manager:false,
716
+ hider:false,
717
+ text:'',
718
+
719
+ winAttrs:'',
720
+ dragAttrs:'',
721
+ titleAttrs:'',
722
+ renameAttrs:'',
723
+ btnAttrs:'',
724
+ hiderAttrs:'',
725
+
726
+ defBtns:[
727
+ ['–',w=>w.hide()],
728
+ ['=',w=>w.toggleFull()],
729
+ ['X',w=>w.close()],
730
+ ],
731
+
732
+ animOpen:'',
733
+ animClose:'',
734
+ animHide:'',
735
+ animShow:'',
736
+ animFullOn:'',
737
+ animFullOff:'',
738
+ _ae:{once:true},
739
+
740
+ _ID(){
741
+ let id;
742
+ // Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
743
+ // !!!: в теории можно задать любой айди
744
+ // ???: проверить при скольки окнах генератор начинает тормозить
745
+ do id=Math.random().toString(36).substring(2,8);
746
+ while (_.wins.has(id));
747
+ return id;
748
+ },
749
+ _winBtn(win,text,func){
750
+ let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
751
+ b.addEventListener('click',()=>func(win));
752
+ return b;
753
+ },
754
+ _hiderBtn(win){
755
+ let title=win.langs!== false ? _.lang.winTitle(_.win.text+win.langs) : `>${win.name}<`,
756
+ b=_.html`<button id=hider${win.id} ${this.hiderAttrs}${title}/button>`;
757
+ b.addEventListener('click',()=>this.show(win));
758
+ return b;
759
+ },
760
+ _initWin: winState=>
761
+ _.drag.init(winState.drag, winState.elem, ()=>_.win.manager.appendChild(winState.elem)),
762
+ open(name,content='',customAttrs=''){
763
+ let winId=this._ID(),
764
+ winState={
765
+ id:winId,
766
+ name:name,
767
+ langs:name,
768
+ state:'opened',
769
+ full:false,
770
+ inRename:false,
771
+ // Если окно новое, координаты полностью нулевые,
772
+ // Нужно чтобы проверять создаётся ли окно и если да то задавать координаты
773
+ onUnfull:{top:0,left:0,width:0,height:0},
774
+ attrs:customAttrs,
775
+ elem:false,
776
+ drag:false,
777
+ content:false,
778
+ };
779
+ return this._opn(winState,content);
780
+ },
781
+ _opn(winState,content=''){
782
+ if (!this.manager || !this.hider) throw new Error('Window managers not inited');
783
+
784
+ let wId=winState.id,
785
+ html=
786
+ _.html`<div id=${wId} ${this.winAttrs} ${winState.attrs}>
787
+ <div style="display:flex;justify-content:space-between;align-items:center"
788
+ ${this.dragAttrs} id=DRAGGER${wId}>
789
+ <span ${this.titleAttrs} id=title${wId}${_.lang.winTitle(_.win.text+winState.name)}/span>
790
+ <div id=btns${wId}></div>
791
+ </div>
792
+ <div id=content${wId} style=overflow:auto;width:100%;height:100%>
793
+ ${content.replace(/\{winId\}/g,wId)}
794
+ </div>
795
+ </div>`,
796
+ btns=html.querySelector(`#btns${wId}`);
797
+ for(let b of this.defBtns) btns.append(this._winBtn(winState,...b));
798
+ html.style.overflow='hidden';
799
+ html.style.resize='both';
800
+
801
+ let anim=this.animOpen;
802
+ if (anim)
803
+ html.addEventListener('animationend',()=>html.classList.remove(anim),this._ae);
804
+ winState.setTitle=nT=>_.win.setTitle(winState,nT);
805
+ winState.toggleFull=e=>_.win.toggleFull(winState);
806
+ winState.close=e=>_.win.close(winState);
807
+ winState.hide=e=>_.win.hide(winState);
808
+ winState.show=e=>_.win.show(winState);
809
+ this.manager.append(html);
810
+
811
+ let win=winState.elem=document.getElementById(wId),
812
+ contentRect=document.getElementById('content'+wId).getBoundingClientRect(),
813
+ windowRect=win.getBoundingClientRect(),
814
+ padX=windowRect.width - contentRect.width,padY=windowRect.height - contentRect.height;
815
+ winState.drag=document.getElementById('DRAGGER'+wId);
816
+ winState.content=document.getElementById('content'+wId);
817
+
818
+ if (winState.onUnfull.width === 0) {
819
+ // Здесь и задаются координаты...
820
+ // Мастера клин кода не выносите мне мозги прошу
821
+ // Оно же работает!!!
822
+ if (!winState.attrs.includes('top')) {
823
+ win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
824
+ win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
825
+ }
826
+ if (!winState.attrs.includes('width')) win.style.height=(win.offsetHeight - padX) + 'px';
827
+ if (!winState.attrs.includes('height')) win.style.width=(win.offsetWidth - padY) + 'px';
828
+ } else
829
+ for (let pos in winState.onUnfull)
830
+ win.style[pos] = winState.onUnfull[pos] + 'px'
831
+
832
+ //this._initWin(winState);
833
+ this._initWin(winState);
834
+ winState.drag.addEventListener('contextmenu',(e)=>{
835
+ e.preventDefault();
836
+ if(e.target.closest('button')) return;
837
+ let wT=document.getElementById('title'+wId);
838
+ if (!winState.inRename){
839
+ wT.innerHTML=`<input ${this.renameAttrs} id=rename${wId} value="${wT.textContent}">`;
840
+ winState.inRename=true;
841
+ }else{
842
+ this.setTitle(winState,document.getElementById('rename'+wId).value);
843
+ winState.inRename=false;
844
+ }
845
+ });
846
+
847
+ if (winState.state === 'hidened') winState.hide();
848
+
849
+ _.wins.set(winState.id, winState);
850
+ return winState;
851
+ },
852
+ setTitle(winState,newT){
853
+ winState.langs=false;
854
+ winState.name=newT;
855
+ let t=document.getElementById('title'+winState.id),
856
+ h=document.getElementById('hider'+winState.id);
857
+ t.innerHTML=newT;
858
+ t.removeAttribute('data-trans');
859
+ if (h){
860
+ h.innerHTML=newT;
861
+ h.removeAttribute('data-trans');
862
+ }
863
+ },
864
+ toggleFull(winState){
865
+ let wEl=winState.elem,
866
+ ws=wEl.style,
867
+ wc=wEl.classList,
868
+ contentRect=document.getElementById('content'+winState.id).getBoundingClientRect(),
869
+ windowRect=wEl.getBoundingClientRect(),
870
+ padX=windowRect.width - contentRect.width,
871
+ padY=windowRect.height - contentRect.height,
872
+ aOn=this.animFullOn,
873
+ aOff=this.animFullOff,
874
+ fd={
875
+ top: windowRect.top, left: windowRect.left,
876
+ width: contentRect.width, height: contentRect.height,
877
+ },
878
+ unful=()=>{
879
+ ws.top=old.top + 'px';
880
+ ws.left=old.left + 'px';
881
+ ws.width=old.width + 'px';
882
+ ws.height=old.height + 'px';
883
+ },
884
+ doFul=()=>{
885
+ if (aOn) wc.remove(aOn);
886
+ winState.full=true;
887
+ winState.onUnfull=fd;
888
+ ws.top=0;
889
+ ws.left=0;
890
+ ws.width=`calc(100% - ${padX}px)`;
891
+ ws.height=`calc(100% - ${padY}px)`;
892
+ winState.drag.onpointerdown=null;
893
+ },
894
+ doUnful=()=>{
895
+ if (aOff) wc.remove(aOff);
896
+ unful();
897
+ winState.full=false;
898
+ this._initWin(winState);
899
+ },
900
+ old=winState.onUnfull;
901
+ if (!winState.full) {
902
+ if (aOn) {
903
+ wc.add(aOn);
904
+ wEl.addEventListener('animationend',doFul,this._ae);
905
+ }else doFul();
906
+ } else {
907
+ if (aOff) {
908
+ wc.add(aOff);
909
+ unful();
910
+ wEl.addEventListener('animationend',doUnful,this._ae);
911
+ }else doUnful();
912
+ }
913
+ },
914
+ close(winState){
915
+ let w=winState.elem,
916
+ remover=()=>{
917
+ let dr=winState.drag,D=document;
918
+ dr.onpointerdown=dr.ontouchmove=null;
919
+ // Удаляем обработчики висящие на документе
920
+ // Если их не удалять рано или поздно случится утечка памяти
921
+ // Я не знаю как я жил во времена 2.0 когда движок только появился
922
+ ['move','up','cancel'].map(e=>D['onpointer'+e]=null);
923
+ w.remove();
924
+ _.wins.delete(winState.id);
925
+ };
926
+ if (w.style.display== 'none'){
927
+ document.getElementById('hider'+winState.id).remove();
928
+ remover();
929
+ }else{
930
+ let anim=this.animClose;
931
+ if(anim){
932
+ w.classList.add(anim);
933
+ w.addEventListener('animationend',remover,this._ae);
934
+ }else
935
+ remover();
936
+ }
937
+ },
938
+ hide(winState){
939
+ let wEl=winState.elem,
940
+ wc=wEl.classList,
941
+ anim=this.animHide,
942
+ hider=()=>{
943
+ wEl.style.display='none';
944
+ if(anim)wc.remove(anim);
945
+ winState.state='hidened';
946
+ this.hider.append(this._hiderBtn(winState));
947
+ }
948
+ if(anim){
949
+ wc.add(anim);
950
+ wEl.addEventListener('animationend',hider,this._ae);
951
+ }else
952
+ hider();
953
+ },
954
+ show(winState){
955
+ let wEl=winState.elem,
956
+ wc=wEl.classList,
957
+ anim=this.animShow,
958
+ hider=document.getElementById('hider'+winState.id),
959
+ shower=()=>{
960
+ if(anim)wc.remove(anim);
961
+ winState.state='opened';
962
+ }
963
+ wEl.style.display='';
964
+ hider.remove();
965
+ if(anim){
966
+ wc.add(anim);
967
+ wEl.addEventListener('animationend',shower,this._ae);
968
+ }else
969
+ shower()
970
+ },
971
+ /*
972
+ * о да, ниже идёт самая крутая фишка которую я готовлю к 2.2
973
+ *
974
+ * СОХРАНЕНИЕ-ВОССТАНОВКА ОКОН
975
+ * Помните автоформы? Здесь я поступил лучше
976
+ * Вы можете полностью сохранить окна, как - решаете вы, но лучше
977
+ * Вместо колбека я теперь просто делаю разовый читатель, так намного гибче
978
+ * Плюсом я делаю разовый восстановитель который возвращает все окна
979
+ * Так тоже в разы гибче, авось у вас в окнах были вебсокеты и их нужно восстановить
980
+ * Проще записать результат а потом прогнать проверку по data-ws атрибутам
981
+ * Или как вы ещё придумаете
982
+ *
983
+ * !!!: Оно работает настолько гибко что в теории можно сделать виртуальные рабочие столы
984
+ */
985
+ read(){
986
+ let store = {};
987
+ for (let [winId, winPre] of _.wins) {
988
+ let win = { ...winPre },
989
+ size=win.onUnfull,
990
+ wEl = win.elem,
991
+ contentRect=win.content.getBoundingClientRect(),
992
+ windowRect=wEl.getBoundingClientRect();
993
+ win.realContent=win.content.innerHTML;
994
+ size.top=windowRect.top;
995
+ size.left=windowRect.left;
996
+ size.height=wEl.offsetHeight - (windowRect.height - contentRect.height);
997
+ size.width=wEl.offsetWidth - (windowRect.width - contentRect.width);
998
+ delete win.elem;
999
+ delete win.drag;
1000
+ delete win.content;
1001
+ store[winId] = win;
1002
+ }
1003
+ return store;
1004
+ },
1005
+ write(state){
1006
+ for (let winId in state) {
1007
+ let win=state[winId],
1008
+ content=win.realContent;
1009
+ delete win.realContent;
1010
+ _.wins.set(winId, win);
1011
+ this._opn(win,content);
1012
+ }
1013
+ return _.wins;
1014
+ },
1015
+ },
1016
+ wins: new Map(),
1017
+ };
1018
+
1019
+ window.addEventListener('popstate',()=>{
1020
+ let l=_.link
1021
+ /*
1022
+ * popstate срабатывает когда:
1023
+ * - пользователь прыгает по истории назад/вперёд
1024
+ * - мы вызываем history.pushState (не replaceState)
1025
+ *
1026
+ * _i различает эти случаи:
1027
+ * true = пользователь прыгнул назад
1028
+ * false = страница пишет свой адрес в ссылку
1029
+ */
1030
+ // ???: некоторые браузеры могут вызывать popstate и при реплейсе
1031
+ if (!l._i) {
1032
+ // здесь происходит перенос команд при popstate
1033
+ // читайте _.lang.get() если хотите узнать почему
1034
+ let nUrl='?' + [l.compile()[0],...l._cmd].join('&');
1035
+ l._i=true;
1036
+ history.replaceState(null,null,nUrl);
1037
+ l.get();
1038
+ } else
1039
+ l._i=false;
1040
+ });
1041
+
1042
+ return _};