newhelper-js 2.1.2 → 2.1.4

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 (3) hide show
  1. package/docs.md +1 -1
  2. package/newHelper.js +542 -370
  3. package/package.json +1 -1
package/docs.md CHANGED
@@ -1,5 +1,5 @@
1
1
  Базовая документация (если поможет)
2
- - Внимание! документация предоставлена к внутренней версии 2.0.9, возможны расхождения с версией 2.1, рекомендуем сверяться с исходным кодом
2
+ - Внимание! документация предоставлена к внутренней версии 2.0.9, возможны расхождения с версией 2.1.4, рекомендуем сверяться с исходным кодом
3
3
 
4
4
  # Роутер (_.link)
5
5
  Роутер здесь завязан на особенном от популярных решений поведении. Берётся query строка, разбивается по "&", и из полученного массива дальше с этой строкой роутер работает обособленно. Сначала идёт элемент 0 который отвечает за страницы и вложенность, последующие элементы являются командами, на них вы можете навешать открытие окон, модалки, или просто всякие дебаг штучки (например отключение проверки наличия капчи при логине или смену языка)
package/newHelper.js CHANGED
@@ -1,25 +1,31 @@
1
1
  /*
2
+ * Перед вами код newHelper.js версии 2.1.4, он построен на базе фабрики
3
+ * Которая начинается с Intl.newHelper=function(){...};
4
+ * Причина использоватся Intl.newHelper банально проста
5
+ * Если я сейчас засираю глобалскоуп одной полу гибкой переменной
6
+ * То почему бы не начать отказываться от засирания глобал скоупа как такового
7
+ * И да, для инициализации ньюхелпера реально нужно писать
8
+ * yourVariable = Intl.newHelper()
9
+ *
2
10
  * Стиль комментариев
3
11
  * FIXME - странное поведение функции, которое желательно бы переделать
4
12
  * ??? - требует уточнения
5
13
  * !!! - обратите внимание
6
14
  *
7
- * Перед вами код newHelper.js версии 2.1.2, он построен на базе гибкой псевдофабрики
8
- * Которая начинается с _=function(){...}();
9
- * Если у вас есть конфликты с Lodash вы можете переименовать _ на всё что вам нужно
15
+ * ???: рассмотреть переход на es6 экспорт вместо вкладывания фабрики в Intl
10
16
  *
11
- * НЕСТАБИЛЬНОЕ API (НЕ РЕКОМЕНДУЕТСЯ ДЛЯ PRODUCTION):
12
- * модуль _.autoForm
13
- * _.win.read()
14
- * _.win.write()
17
+ * ???: Последний глобальный эвент лисенер можно вынести внутрь _.link.get()
15
18
  *
16
- * УДАЛЕНО (вернётся позже):
17
- * модуль _.err
19
+ * Новые модули, готовятсяк релизу в 2.2
20
+ * их апи может быть чуть чуть нестабильно:
21
+ * form
22
+ * tables
23
+ * drag (портирован из окон)
24
+ * pipe/pipeAsync
18
25
  */
19
26
 
20
-
21
- _=function(){let _={
22
- link:{
27
+ Intl.newHelper=function() {let _ = {
28
+ link: {
23
29
  /*
24
30
  * МОДУЛЬ ССЫЛОК
25
31
  *
@@ -34,41 +40,41 @@ link:{
34
40
  *
35
41
  * !!!: в функции get() работает весь роутинг, в т.ч. вложенный для страниц
36
42
  */
37
- basePage:()=>{},
38
- defTitle:'',
39
- actions:{},
40
- commands:{},
43
+ basePage: ()=>{},
44
+ defTitle: '',
45
+ actions: {},
46
+ commands: {},
41
47
 
42
48
  _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;
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;
50
56
  history.pushState(null,null,'?'+link.join('&'));
51
57
  }
52
- this._i=false;
58
+ this._i = false;
53
59
  },
54
- add(cmd){
55
- let link=this.compile();
56
- if (!link.includes(cmd)){
60
+ add(cmd) {
61
+ let link = this.compile();
62
+ if (!link.includes(cmd)) {
57
63
  link.push(cmd);
58
64
  this._cmd.push(cmd);
59
65
  history.replaceState(null,null,'?'+link.join('&'));
60
66
  }
61
67
  },
62
- remove(cmd){
63
- let link=this.compile();
68
+ remove(cmd) {
69
+ let link = this.compile();
64
70
  if (link.includes(cmd)){
65
- let c=this._cmd;
71
+ let c = this._cmd;
66
72
  link.splice(link.indexOf(cmd),1);
67
73
  c.splice(c.indexOf(cmd),1);
68
74
  history.replaceState(null,null,'?'+link.join('&'));
69
75
  }
70
76
  },
71
- get(){
77
+ get() {
72
78
  /*
73
79
  * Страницы бросают ошибку чтобы вызвать базовую страницу
74
80
  * Команды тем временем так не делают
@@ -77,40 +83,42 @@ link:{
77
83
  * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
78
84
  * Сделано это для переноса команд при прыжках по истории
79
85
  */
80
- let links=this.compile(),
81
- [firstKey,fisrtValue]=links[0].split('='),
82
- cmds=links.slice(1);
86
+ let links = this.compile(),
87
+ [ firstKey, fisrtValue ] = links[0].split('='),
88
+ cmds = links.slice(1);
83
89
  try {
84
- let dirs=firstKey.split('/'),
85
- dir=this.actions,
86
- main=dir[firstKey];
87
- if (!firstKey.includes('/')){
90
+ let dirs = firstKey.split('/'),
91
+ dir = this.actions,
92
+ main = dir[firstKey];
93
+ if (!firstKey.includes('/')) {
88
94
  main(fisrtValue);
89
95
  } else {
90
96
  for (let p of dirs){
91
- let kDir=dir[p+'/'];
97
+ let kDir = dir[p+'/'];
92
98
  if (kDir)
93
- dir=kDir;
99
+ dir = kDir;
94
100
  else{
95
101
  dir[p](fisrtValue);
96
102
  break;
97
103
  }
98
104
  }
99
105
  }
100
- }catch(e){
106
+ } catch (e) {
101
107
  this.basePage();
102
108
  throw e;
103
109
  }
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
+ 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!`))
110
118
  });
111
119
  },
112
120
  },
113
- lazy:{
121
+ lazy: {
114
122
  /*
115
123
  * МОДУЛЬ ЛЕНИ
116
124
  *
@@ -123,12 +131,8 @@ lazy:{
123
131
  *
124
132
  * ???: будет ли легче создавать лень в легаси проектах через es6 импорты
125
133
  */
126
- loaded:{},
127
- load(url,...args){
128
- let key=url.split('?')[0], // отсекаем параметры, чтобы не дублировать
129
- state=this.loaded,
130
- c=state[key];
131
-
134
+ loaded: {},
135
+ load(url, ...args) {
132
136
  /*
133
137
  * ...args передаются в Promise.resolve(args)
134
138
  * Это позволяет делать _.lazy.load('script.js', 'данные', 'для', 'колбека')
@@ -141,45 +145,54 @@ lazy:{
141
145
  *
142
146
  * Это защита от двойной загрузки одного скрипта
143
147
  */
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;
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;
152
160
  resolve(args);
153
161
  };
154
- scr.onerror=()=>{
162
+ scr.onerror = ()=>{
155
163
  delete state[key];
156
164
  reject(new Error('Failed to load '+url));
157
165
  };
158
- _.$.D.head.append(scr);
166
+ document.head.append(scr);
159
167
  });
160
- state[key]=pr;
161
- return pr;
168
+ state[key] = promise;
169
+ return promise;
162
170
  },
163
- register(scr,funcs){
164
- if (!Array.isArray(funcs)) return new Error('Array required for register');
171
+ register(script, funcs) {
172
+ if (!Array.isArray(funcs))
173
+ return new Error('Array required for register');
165
174
 
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
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
+ }
172
183
  },
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];
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];
179
192
  throw new Error(`Function ${fn} not loaded from ${scr}`);
180
193
  },
181
194
  },
182
- lang:{
195
+ lang: {
183
196
  /*
184
197
  * МОДУЛЬ ПЕРЕВОДОВ
185
198
  *
@@ -191,44 +204,35 @@ lang:{
191
204
  * Так удобнее отображать динамичные данные на сайтах
192
205
  * Например никнейм пользователя
193
206
  */
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;
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;
215
217
  }),
216
218
  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;
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;
226
229
  else if (['INPUT','TEXTAREA'].includes(tag))
227
- e[e.type=== 'submit' ? 'value' :'placeholder']=text;
228
- else e.innerHTML=text;
230
+ el[ el.type === 'submit' ? 'value' : 'placeholder' ] = text;
231
+ else
232
+ el.innerHTML = text;
229
233
  }
230
234
  // возвращаем для последующей обработки пакета, например для сохранения в _.storage
231
- return p;
235
+ return packet;
232
236
  },
233
237
 
234
238
  /*
@@ -242,30 +246,25 @@ lang:{
242
246
  *
243
247
  * !!!: если ключа в пакете нету, будет выброшен warning
244
248
  */
245
- attr:(i)=>` data-trans="${i}"`,
246
- from:i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
249
+ attr: i=>` data-trans="${i}"`,
250
+ from: i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
247
251
 
248
252
  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
- */
253
+ submit: i=>_.lang.attr(i)+`value="${_.lang.from(i)}">`, // <input type=submit>
255
254
  input: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}">`,
256
255
  textarea: i=>_.lang.attr(i)+`placeholder="${_.lang.from(i)}"><`,
257
256
  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='';
257
+ winTitle(i) {
258
+ let text = this.from(i),
259
+ dataTrans = this.attr(i);
260
+ if (text == null || text == '') {
261
+ text = i;
262
+ dataTrans = '';
264
263
  }
265
- return `${dT}>${text}<`;
264
+ return `${dataTrans}>${text}<`;
266
265
  },
267
266
  },
268
- http:{
267
+ http: {
269
268
  /*
270
269
  * HTTP-КЛИЕНТ
271
270
  *
@@ -278,50 +277,46 @@ http:{
278
277
  * Как пример Authorization: 'your token'
279
278
  * ???: добавить возможность игнорировать дефолтные хедеры
280
279
  */
281
- defaultHeaders:{},
282
- req(method,url,data='',headers={},fileProgressElement=false){
283
- return new Promise((resolve,reject)=>{
284
- let xhr=new XMLHttpRequest();
280
+ defaultHeaders: {},
281
+ req(method, url, data = '', headers = {}, fileProgressElement = false) {
282
+ return new Promise((resolve, reject)=>{
283
+ let xhr = new XMLHttpRequest();
285
284
 
286
- xhr.open(method,url);
285
+ xhr.open(method, url);
287
286
 
288
- let allHeaders={...this.defaultHeaders,...headers};
287
+ let allHeaders = { ...this.defaultHeaders, ...headers };
289
288
  for (let header in allHeaders)
290
- xhr.setRequestHeader(header,allHeaders[header]);
289
+ xhr.setRequestHeader(header, allHeaders[header]);
291
290
 
292
291
  // !!!: fileProgressElement ожидает <progress> элемент без min/max
293
292
  // Потому что value от 0 до 1
294
293
  if (fileProgressElement)
295
- xhr.upload.onprogress=(e)=>{
296
- if (e.lengthComputable){
297
- let percentage=(e.loaded / e.total);
298
- fileProgressElement.setAttribute('value',percentage);
294
+ xhr.upload.onprogress= e=>{
295
+ if (e.lengthComputable) {
296
+ let percentage = (e.loaded / e.total);
297
+ fileProgressElement.setAttribute('value', percentage);
299
298
  }
300
299
  };
301
300
 
302
- xhr.onreadystatechange=()=>{
301
+ xhr.onreadystatechange= ()=>{
303
302
  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);
303
+ if (xhr.status >= 200 && xhr.status < 300)
304
+ resolve(xhr.response);
305
+ else
306
+ reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
306
307
  };
307
- xhr.onerror=()=>reject(new Error('Network error'),xhr);
308
+ xhr.onerror = ()=>
309
+ reject(new Error('Network error'), xhr);
308
310
 
309
311
  xhr.send(data);
310
312
  });
311
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)
312
318
  },
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){
319
+ html(strs, ...args) {
325
320
  /*
326
321
  * Шаблонные строки в DOM
327
322
  *
@@ -334,82 +329,229 @@ html(strs,...args){
334
329
  * - Быстрее чем createElement для сложных структур
335
330
  * - Банально удобнее createElement для сложных древ
336
331
  */
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)
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)
349
360
  return content.firstChild;
350
361
  return content;
351
362
  },
352
- autoForm:{
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: {
353
388
  /*
354
389
  * АВТОСОХРАНЕНИЕ ФОРМ
355
390
  *
356
391
  * Позволяет сохранять состояние формы на случай
357
392
  * Если в офисе внезапно выключат свет
358
393
  *
359
- * Реализовывать удаление читателя событий я не стал
360
- * Зачем удалять обработчик если он вешается на форму а не на Document?
394
+ * ???: может сделать более полноценный модуль форм
395
+ * с встроенной валидацией, или чем нибуть ещё
361
396
  */
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);
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;
378
407
  });
408
+ return data;
379
409
  },
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);
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
+ });
394
430
  else
395
- el.value=v;
431
+ el.value = value;
396
432
  });
397
433
  return data;
398
434
  },
399
435
  },
400
- storage:class{
401
- constructor(strg,name){
402
- this._=strg;
403
- this.n=name;
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;
404
519
  }
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));
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));
411
526
  },
412
- hotkeys:{
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: {
413
555
  /*
414
556
  * ГОРЯЧИЕ КЛАВИШЫ
415
557
  *
@@ -425,78 +567,126 @@ hotkeys:{
425
567
  * Вы же не хотите чтобы у вас тормозил поток с 100+ хоткеями
426
568
  * Из-за простого печатанья?
427
569
  */
428
- keys:{},
429
- _holds:new Set(),
430
- _:false,
570
+ keys: new Map(),
571
+ _holds: new Set(),
572
+ _: false,
431
573
 
432
- _parse:combo=>combo.split('+').map(k=>k.trim()),
574
+ _parse: combo => combo.split('+').map(k=>k.trim()),
433
575
  _match(keys) {
434
576
  // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
435
577
  for (let k of keys) if (!this._holds.has(k)) return false;
436
578
  return true;
437
579
  },
438
580
  _init() {
439
- if (this._) return;
440
- _.$.on(_.$.D,'keydown',e=>{
581
+ if (this._)
582
+ return;
583
+ document.addEventListener('keydown', e=>{
441
584
  this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
442
585
  // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
443
586
 
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);
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);
451
593
  }
452
594
  }
453
595
  });
454
- _.$.on(_.$.D,'keyup',e=>{
596
+ document.addEventListener('keyup', e=>{
455
597
  this._holds.delete(e.code);
456
598
 
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);
599
+ for (let hotkey of this.keys.values()) {
600
+ if (hotkey.active && !this._match(hotkey.keys)) {
601
+ hotkey.active=false;
602
+ hotkey.release(e);
462
603
  }
463
604
  }
464
605
  });
465
- _.$.on(window,'blur',()=>{
606
+ window.addEventListener('blur', e=>{
466
607
  /*
467
608
  * При переключении в другое окно автоматического keyup не будет
468
609
  * Поэтому сбрасываем всё принудительно, мало ли
469
610
  */
470
- for (let combo in this.keys) {
471
- let h=this.keys[combo];
472
- if (h.active) {
473
- h.active=false;
474
- h.release();
611
+ for (let hotkey of this.keys.values()) {
612
+ if (hotkey.active) {
613
+ hotkey.active = false;
614
+ hotkey.release();
475
615
  }
476
616
  }
477
617
  this._holds.clear();
478
618
  });
479
619
  this._=true;
480
620
  },
481
- on(combo,press,release) {
621
+ on(combo, press, release) {
482
622
  this._init();
483
- let keys=this._parse(combo);
623
+ let keys = this._parse(combo);
484
624
 
485
- this.keys[combo]={
625
+ this.keys.set(combo, {
486
626
  keys,
487
627
  // press/releace по умолчанию пустышки для сокращения синаксиса
488
628
  press: press || (()=>{}),
489
629
  release: release || (()=>{}),
490
630
  active: false
491
- };
631
+ });
492
632
 
493
633
  return this;
494
634
  },
495
635
  off(combo) {
496
- delete this.keys[combo];
636
+ this.keys.delete(combo);
497
637
  return this;
498
638
  },
499
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
+ },
500
690
  win:{
501
691
  /*
502
692
  * МОДУЛЬ ОКОН
@@ -524,6 +714,7 @@ win:{
524
714
  */
525
715
  manager:false,
526
716
  hider:false,
717
+ text:'',
527
718
 
528
719
  winAttrs:'',
529
720
  dragAttrs:'',
@@ -552,63 +743,25 @@ win:{
552
743
  // !!!: в теории можно задать любой айди
553
744
  // ???: проверить при скольки окнах генератор начинает тормозить
554
745
  do id=Math.random().toString(36).substring(2,8);
555
- while (_.wins[id]);
746
+ while (_.wins.has(id));
556
747
  return id;
557
748
  },
558
749
  _winBtn(win,text,func){
559
750
  let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
560
- _.$.on(b,'click',()=>func(win));
751
+ b.addEventListener('click',()=>func(win));
561
752
  return b;
562
753
  },
563
754
  _hiderBtn(win){
564
- let title=win.langs!== false ? _.lang.win('WINDOW-'+win.langs) : `>${win.name}<`,
755
+ let title=win.langs!== false ? _.lang.winTitle(_.win.text+win.langs) : `>${win.name}<`,
565
756
  b=_.html`<button id=hider${win.id} ${this.hiderAttrs}${title}/button>`;
566
- _.$.on(b,'click',()=>this.show(win));
757
+ b.addEventListener('click',()=>this.show(win));
567
758
  return b;
568
759
  },
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
- },
760
+ _initWin: winState=>
761
+ _.drag.init(winState.drag, winState.elem, ()=>_.win.manager.appendChild(winState.elem)),
609
762
  open(name,content='',customAttrs=''){
610
763
  let winId=this._ID(),
611
- winState=_.wins[winId]={
764
+ winState={
612
765
  id:winId,
613
766
  name:name,
614
767
  langs:name,
@@ -625,79 +778,82 @@ win:{
625
778
  };
626
779
  return this._opn(winState,content);
627
780
  },
628
- _opn(w,content=''){
781
+ _opn(winState,content=''){
629
782
  if (!this.manager || !this.hider) throw new Error('Window managers not inited');
630
783
 
631
- let wId=w.id,
784
+ let wId=winState.id,
632
785
  html=
633
- _.html`<div id=${wId} ${this.winAttrs} ${w.attrs}>
786
+ _.html`<div id=${wId} ${this.winAttrs} ${winState.attrs}>
634
787
  <div style="display:flex;justify-content:space-between;align-items:center"
635
788
  ${this.dragAttrs} id=DRAGGER${wId}>
636
- <span ${this.titleAttrs} id=title${wId}${_.lang.win('WINDOW-'+w.name)}/span>
789
+ <span ${this.titleAttrs} id=title${wId}${_.lang.winTitle(_.win.text+winState.name)}/span>
637
790
  <div id=btns${wId}></div>
638
791
  </div>
639
792
  <div id=content${wId} style=overflow:auto;width:100%;height:100%>
640
793
  ${content.replace(/\{winId\}/g,wId)}
641
794
  </div>
642
795
  </div>`,
643
- btns=_.$.q(`#btns${wId}`,html);
644
- for(let b of this.defBtns) btns.append(this._winBtn(w,...b));
796
+ btns=html.querySelector(`#btns${wId}`);
797
+ for(let b of this.defBtns) btns.append(this._winBtn(winState,...b));
645
798
  html.style.overflow='hidden';
646
799
  html.style.resize='both';
647
800
 
648
801
  let anim=this.animOpen;
649
802
  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);
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);
656
809
  this.manager.append(html);
657
810
 
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);
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);
663
817
 
664
- if (w.onUnfull.width === 0) {
818
+ if (winState.onUnfull.width === 0) {
665
819
  // Здесь и задаются координаты...
666
820
  // Мастера клин кода не выносите мне мозги прошу
667
821
  // Оно же работает!!!
668
- if (!w.attrs.includes('top')) {
822
+ if (!winState.attrs.includes('top')) {
669
823
  win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
670
824
  win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
671
825
  }
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';
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';
674
828
  } else
675
- for (let pos in w.onUnfull)
676
- win.style[pos] = w.onUnfull[pos] + 'px'
829
+ for (let pos in winState.onUnfull)
830
+ win.style[pos] = winState.onUnfull[pos] + 'px'
677
831
 
678
- this._initWin(w);
679
- _.$.on(w.drag,'contextmenu',(e)=>{
832
+ //this._initWin(winState);
833
+ this._initWin(winState);
834
+ winState.drag.addEventListener('contextmenu',(e)=>{
680
835
  e.preventDefault();
681
836
  if(e.target.closest('button')) return;
682
- let wT=_.$.id('title'+wId);
683
- if (!w.inRename){
837
+ let wT=document.getElementById('title'+wId);
838
+ if (!winState.inRename){
684
839
  wT.innerHTML=`<input ${this.renameAttrs} id=rename${wId} value="${wT.textContent}">`;
685
- w.inRename=true;
840
+ winState.inRename=true;
686
841
  }else{
687
- this.setTitle(w,_.$.id('rename'+wId).value);
688
- w.inRename=false;
842
+ this.setTitle(winState,document.getElementById('rename'+wId).value);
843
+ winState.inRename=false;
689
844
  }
690
845
  });
691
846
 
692
- if (w.state === 'hidened') w.hide();
847
+ if (winState.state === 'hidened') winState.hide();
693
848
 
694
- return w;
849
+ _.wins.set(winState.id, winState);
850
+ return winState;
695
851
  },
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);
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);
701
857
  t.innerHTML=newT;
702
858
  t.removeAttribute('data-trans');
703
859
  if (h){
@@ -705,19 +861,19 @@ win:{
705
861
  h.removeAttribute('data-trans');
706
862
  }
707
863
  },
708
- toggleFull(win){
709
- let wEl=win.elem,
864
+ toggleFull(winState){
865
+ let wEl=winState.elem,
710
866
  ws=wEl.style,
711
867
  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,
868
+ contentRect=document.getElementById('content'+winState.id).getBoundingClientRect(),
869
+ windowRect=wEl.getBoundingClientRect(),
870
+ padX=windowRect.width - contentRect.width,
871
+ padY=windowRect.height - contentRect.height,
716
872
  aOn=this.animFullOn,
717
873
  aOff=this.animFullOff,
718
874
  fd={
719
- top: rect.top, left: rect.left,
720
- width: cont.width, height: cont.height,
875
+ top: windowRect.top, left: windowRect.left,
876
+ width: contentRect.width, height: contentRect.height,
721
877
  },
722
878
  unful=()=>{
723
879
  ws.top=old.top + 'px';
@@ -727,103 +883,118 @@ win:{
727
883
  },
728
884
  doFul=()=>{
729
885
  if (aOn) wc.remove(aOn);
730
- win.full=true;
731
- win.onUnfull=fd;
886
+ winState.full=true;
887
+ winState.onUnfull=fd;
732
888
  ws.top=0;
733
889
  ws.left=0;
734
890
  ws.width=`calc(100% - ${padX}px)`;
735
891
  ws.height=`calc(100% - ${padY}px)`;
736
- win.drag.onpointerdown=null;
892
+ winState.drag.onpointerdown=null;
737
893
  },
738
894
  doUnful=()=>{
739
895
  if (aOff) wc.remove(aOff);
740
896
  unful();
741
- win.full=false;
742
- this._initWin(win);
897
+ winState.full=false;
898
+ this._initWin(winState);
743
899
  },
744
- old=win.onUnfull;
745
- if (!win.full) {
900
+ old=winState.onUnfull;
901
+ if (!winState.full) {
746
902
  if (aOn) {
747
903
  wc.add(aOn);
748
- _.$.on(wEl,'animationend',doFul,this._ae);
904
+ wEl.addEventListener('animationend',doFul,this._ae);
749
905
  }else doFul();
750
906
  } else {
751
907
  if (aOff) {
752
908
  wc.add(aOff);
753
909
  unful();
754
- _.$.on(wEl,'animationend',doUnful,this._ae);
910
+ wEl.addEventListener('animationend',doUnful,this._ae);
755
911
  }else doUnful();
756
912
  }
757
913
  },
758
- close(win){
759
- let w=win.elem,
914
+ close(winState){
915
+ let w=winState.elem,
760
916
  remover=()=>{
761
- let dr=win.drag,D=_.$.D;
917
+ let dr=winState.drag,D=document;
762
918
  dr.onpointerdown=dr.ontouchmove=null;
763
919
  // Удаляем обработчики висящие на документе
764
920
  // Если их не удалять рано или поздно случится утечка памяти
765
921
  // Я не знаю как я жил во времена 2.0 когда движок только появился
766
922
  ['move','up','cancel'].map(e=>D['onpointer'+e]=null);
767
923
  w.remove();
768
- delete _.wins[win.id];
924
+ _.wins.delete(winState.id);
769
925
  };
770
926
  if (w.style.display== 'none'){
771
- _.$.id('hider'+win.id).remove();
927
+ document.getElementById('hider'+winState.id).remove();
772
928
  remover();
773
929
  }else{
774
930
  let anim=this.animClose;
775
931
  if(anim){
776
932
  w.classList.add(anim);
777
- _.$.on(w,'animationend',remover,this._ae);
933
+ w.addEventListener('animationend',remover,this._ae);
778
934
  }else
779
935
  remover();
780
936
  }
781
937
  },
782
- hide(win){
783
- let wEl=win.elem,
938
+ hide(winState){
939
+ let wEl=winState.elem,
784
940
  wc=wEl.classList,
785
941
  anim=this.animHide,
786
942
  hider=()=>{
787
943
  wEl.style.display='none';
788
944
  if(anim)wc.remove(anim);
789
- win.state='hidened';
790
- this.hider.append(this._hiderBtn(win));
945
+ winState.state='hidened';
946
+ this.hider.append(this._hiderBtn(winState));
791
947
  }
792
948
  if(anim){
793
949
  wc.add(anim);
794
- _.$.on(wEl,'animationend',hider,this._ae);
950
+ wEl.addEventListener('animationend',hider,this._ae);
795
951
  }else
796
952
  hider();
797
953
  },
798
- show(win){
799
- let wEl=win.elem,
954
+ show(winState){
955
+ let wEl=winState.elem,
800
956
  wc=wEl.classList,
801
957
  anim=this.animShow,
802
- hider=_.$.id('hider'+win.id),
958
+ hider=document.getElementById('hider'+winState.id),
803
959
  shower=()=>{
804
960
  if(anim)wc.remove(anim);
805
- win.state='opened';
961
+ winState.state='opened';
806
962
  }
807
963
  wEl.style.display='';
808
964
  hider.remove();
809
965
  if(anim){
810
966
  wc.add(anim);
811
- _.$.on(wEl,'animationend',shower,this._ae);
967
+ wEl.addEventListener('animationend',shower,this._ae);
812
968
  }else
813
969
  shower()
814
970
  },
971
+ /*
972
+ * о да, ниже идёт самая крутая фишка которую я готовлю к 2.2
973
+ *
974
+ * СОХРАНЕНИЕ-ВОССТАНОВКА ОКОН
975
+ * Помните автоформы? Здесь я поступил лучше
976
+ * Вы можете полностью сохранить окна, как - решаете вы, но лучше
977
+ * Вместо колбека я теперь просто делаю разовый читатель, так намного гибче
978
+ * Плюсом я делаю разовый восстановитель который возвращает все окна
979
+ * Так тоже в разы гибче, авось у вас в окнах были вебсокеты и их нужно восстановить
980
+ * Проще записать результат а потом прогнать проверку по data-ws атрибутам
981
+ * Или как вы ещё придумаете
982
+ *
983
+ * !!!: Оно работает настолько гибко что в теории можно сделать виртуальные рабочие столы
984
+ */
815
985
  read(){
816
986
  let store = {};
817
- for (let winId in _.wins) {
818
- let win={..._.wins[winId]},
987
+ for (let [winId, winPre] of _.wins) {
988
+ let win = { ...winPre },
819
989
  size=win.onUnfull,
820
990
  wEl = win.elem,
821
- c=_.$.cliRect(win.content),r=_.$.cliRect(wEl);
991
+ contentRect=win.content.getBoundingClientRect(),
992
+ windowRect=wEl.getBoundingClientRect();
822
993
  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);
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);
827
998
  delete win.elem;
828
999
  delete win.drag;
829
1000
  delete win.content;
@@ -836,15 +1007,16 @@ win:{
836
1007
  let win=state[winId],
837
1008
  content=win.realContent;
838
1009
  delete win.realContent;
839
- _.wins[winId] = win;
1010
+ _.wins.set(winId, win);
840
1011
  this._opn(win,content);
841
1012
  }
1013
+ return _.wins;
842
1014
  },
843
1015
  },
844
- wins:{},
1016
+ wins: new Map(),
845
1017
  };
846
1018
 
847
- _.$.on(window,'popstate',()=>{
1019
+ window.addEventListener('popstate',()=>{
848
1020
  let l=_.link
849
1021
  /*
850
1022
  * popstate срабатывает когда:
@@ -867,4 +1039,4 @@ _.$.on(window,'popstate',()=>{
867
1039
  l._i=false;
868
1040
  });
869
1041
 
870
- return _}();
1042
+ return _};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newhelper-js",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "сверх лёгкая библиотека для построения админок",
5
5
  "keywords": [
6
6
  "admin",