newhelper-js 2.1.0 → 2.1.2

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.
package/README-en.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # newHelper.js
2
2
  - [русская версия](README.md)
3
3
  - A library for creating ultra-lightweight yet highly functional admin panels, one of the best examples of Object Hub. The total bundle of the entire site is only 280 kb of pure code, and the internal admin panel with all HTML+CSS+JS weighs only 25 kilobytes (with gzip, the size is even smaller). I try to keep the library in a "Modular Monolith" state.
4
- Current version - 2.1, 13.6kb
4
+ Current version - 2.1, with good documentation in the code
5
+ ## Included modules:
6
+ - Advanced window engine (resize, fullscreen, taskbar, 6.29 KB)
7
+ - Convenient hotkeys (press/release callbacks, 1.16 KB)
8
+ - Router (1.48 KB)
9
+ - Ultra-light i18n analog (1.30 KB)
10
+ - Custom lazy-load (0.97 KB)
11
+ - Error catching module (0.54 KB)
12
+ - Simple HTTP client with file upload progress (0.82 KB)
13
+ - DOM helpers (0.61 KB)
14
+ - Storage isolation (0.31 KB)
5
15
 
6
- # Features
16
+ ## Features
7
17
  - The library uses a classic connection with a script tag and requires no bundlers/compilers. But if you really want to, you can configure it, but why? It's the assembly language of the web world.
8
18
  - Dependencies? Nope, it's vanilla ES6.
9
19
  - The library size mentioned at the beginning is the size of the source code, so minification + gzip will reduce the size even further.
package/README.md CHANGED
@@ -1,14 +1,25 @@
1
1
  # newHelper.js
2
2
  - [english version](README-en.md)
3
+ - [документация](docs.md)
3
4
  - Библиотека для создания сверхлёгких но очень функциональных админ панелей, один из лучших примеров Object hub, Общий бандл всего сайта составляет всего 280 кб чистого кода, внутренная админ панель со всем html+css+js весит всего 25 килобайт (при gzip размер ещё ниже). Я стараюсь придерживать библиотеку в состоянии "Модульного монолита".
4
- Текущая версия - 2.1, 13.6кб
5
+ Текущая версия - 2.1.2, с хорошей документацией в коде
6
+ ## Модули в комплекте
7
+ - Широкий движок окон (ресайз, разворот на весь экран, taskbar, 6.29 кб)
8
+ - Удобные горячие клавиши (press/release колбеки, 1.16 кб)
9
+ - Роутер (1.48 кб)
10
+ - Ультралёгкий аналог i18n (1.30 кб)
11
+ - Самописный lazy-load (0.97 кб)
12
+ - Модуль отлова ошибок (0.54 кб)
13
+ - Простой http клиент с прогрессом загрузки файлов (0.82 кб)
14
+ - DOM-хелперы (0.61 кб)
15
+ - Изоляция над Storage (0.31 кб)
5
16
 
6
- Особенности
17
+ ## Особенности
7
18
  - Библиотека использует классическое подключение с тегом script и не требует никаких сборщиков/компиляторов. Но если очень хочется вы можете настроить его, но зачем? это же ассемблер из мира веб
8
- - Зависимости? неа, это ванильный ES6
19
+ - Зависимости? неа, это ванильный ES8
9
20
  - Указанный в начале размер библиотеки является размером исходного кода, так что минификация+gzip уменьшат размер ещё сильнее
10
21
  - Из-за минималистичности кода вы можете спокойно переопределить любой встроенный метод под свои нужны. В крайнем случае можно прибегнуть к модификации кода библиотеки, но такое мы не рекомендуем делать в случае если вы собираетесь обновляться до самых последних версий
11
- - Весь код написан внутри одного "_" и использованием всего трёх глобальных читателей событий. Из-за этого конфликты с остальными библиотеками минимальны, если вы готовы то можете пробовать запустить newHelper вместе с jQuery или если вы капец как хотите реактивность то пробуйте в связке с ультралёгкими реактивными фреймворками (у меня на слуху из адекватных alpine.js)
22
+ - Весь код написан внутри одного "_" и использованием всего трёх глобальных читателей событий. Из-за этого конфликты с остальными библиотеками минимальны, если вы готовы то можете пробовать запустить newHelper вместе с jQuery или если вы очень хотите реактивность то пробуйте в связке с ультралёгкими реактивными фреймворками (у меня на слуху из адекватных alpine.js)
12
23
  - Если вы последователь "14 килобайтных сайтов" то можете смело прогнать библиотеку через минификатор, минифицированный код библиотеки не должен весить больше 5 килобайт, а если отбросить ненужные вам модули и то меньше (например код движка окон составляет приблизительно 50% всего кода библиотеки, если вам не нужен движок окон - оставляйте 2 килобайта gzip-ядра)
13
24
  - Если вы научитесь грамотно использовать ленивую загрузку, то ваши и без того лёгкие сайты станут в разы легче (тот же стартовый трафик Object Hub уменьшился 330кб до 180кб при старте из-за разбития всего на lazyload модули)
14
25
  - На базе этой библиотеки работает парочка моих сайтов не связанных с админ панелями - GDPS Helper (каталог приватных серверов geometry dash, но там тоже идёт админка для демонлистов), Object hub (каталог с элементами вики движка и hr-платформы для OSC сообщества, вики движок во всю использует окна вместо костылей mediawiki). Если захотеть можно не только админку за 2 часа собрать, но и написать мегасайт за пару недель
package/newHelper.js CHANGED
@@ -1,15 +1,48 @@
1
- _={};
2
- _.ver='2.1';
3
- _.link={
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
+ */
4
37
  basePage:()=>{},
5
38
  defTitle:'',
6
39
  actions:{},
7
40
  commands:{},
8
- _cmd:[],
9
41
 
10
- _i: true,
42
+ _i: true, // _i - блокировщик pushState в set()
43
+ _cmd:[],
11
44
  compile:()=>location.search.replace('?','').split('&'),
12
- set(page,title=_.link.defTitle){
45
+ set(page,title=this.defTitle){
13
46
  if (title) _.$.D.title=title;
14
47
  if (!this._i){
15
48
  let link=this.compile();
@@ -36,47 +69,78 @@ _.link={
36
69
  }
37
70
  },
38
71
  get(){
39
- let cfg=_.link,
40
- links=this.compile(),
41
- [fKey,fVal]=links[0].split('='),
72
+ /*
73
+ * Страницы бросают ошибку чтобы вызвать базовую страницу
74
+ * Команды тем временем так не делают
75
+ * Потому что сломанная команда не так страшна как сломанная страница
76
+ *
77
+ * При popstate команды берутся из хранилища _cmd, вместо самой ссылки
78
+ * Сделано это для переноса команд при прыжках по истории
79
+ */
80
+ let links=this.compile(),
81
+ [firstKey,fisrtValue]=links[0].split('='),
42
82
  cmds=links.slice(1);
43
83
  try {
44
- let dirs=fKey.split('/'),
45
- dir=cfg.actions,
46
- main=dir[fKey];
47
- if (!fKey.includes('/')){
48
- main(fVal);
84
+ let dirs=firstKey.split('/'),
85
+ dir=this.actions,
86
+ main=dir[firstKey];
87
+ if (!firstKey.includes('/')){
88
+ main(fisrtValue);
49
89
  } else {
50
90
  for (let p of dirs){
51
91
  let kDir=dir[p+'/'];
52
92
  if (kDir)
53
93
  dir=kDir;
54
94
  else{
55
- dir[p](fVal);
95
+ dir[p](fisrtValue);
56
96
  break;
57
97
  }
58
98
  }
59
99
  }
60
100
  }catch(e){
61
- _.link.basePage();
101
+ this.basePage();
62
102
  throw e;
63
103
  }
64
- _.link._cmd=cmds;
65
- cmds.forEach(kPre=>{
66
- let [key,val]=kPre.split('=');
67
- let cmd=cfg.commands[key];
68
- if (cmd)
69
- cmd(val);
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!`))
70
110
  });
71
111
  },
72
- };
73
- _.lazy={
112
+ },
113
+ lazy:{
114
+ /*
115
+ * МОДУЛЬ ЛЕНИ
116
+ *
117
+ * Создаёт в глобальной области видимости прокси функции
118
+ * Вызывающие загрузку скрипта с внещним модулем
119
+ * Был сделан через глобальную область, так намного проще создавать лень
120
+ *
121
+ * !!!: Функции обёртки в register() должны быть повешаны на window
122
+ * Иначе lazy._ провалится в рекурсию ошибок, не наступайте на мои грабли
123
+ *
124
+ * ???: будет ли легче создавать лень в легаси проектах через es6 импорты
125
+ */
74
126
  loaded:{},
75
127
  load(url,...args){
76
- let key=url.split('?')[0],
128
+ let key=url.split('?')[0], // отсекаем параметры, чтобы не дублировать
77
129
  state=this.loaded,
78
130
  c=state[key];
79
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
+ */
80
144
  if (c=== true) return Promise.resolve(args);
81
145
  if (c instanceof Promise) return c.then(()=>args);
82
146
 
@@ -100,42 +164,62 @@ _.lazy={
100
164
  if (!Array.isArray(funcs)) return new Error('Array required for register');
101
165
 
102
166
  for (let fn of funcs)
103
- window[fn]=(...a)=>_.lazy._(scr,fn).then(f=>f(...a));
167
+ window[fn]=(...a)=>this._(scr,fn).then(f=>f(...a));
168
+ // ???: добавить вложенность с созданием объектов-обёрток
104
169
 
105
- console.info('_.lazy> Applied lazy '+scr+' with this functions:',funcs);
170
+ console.info('lazy> Applied lazy '+scr+' with this functions:',funcs);
171
+ // ???: не мешает ли тут console.info
106
172
  },
107
173
  async _(scr,fn){
108
174
  let w=window,
109
- wrpr=w[fn];
110
- try{await _.lazy.load(scr)}
175
+ wrapper=w[fn];
176
+ try{await this.load(scr)}
111
177
  catch(e){throw e}
112
- if (wrpr!== w[fn]) return w[fn];
178
+ if (wrapper!== w[fn]) return w[fn];
113
179
  throw new Error(`Function ${fn} not loaded from ${scr}`);
114
180
  },
115
- };
116
- _.lang={
181
+ },
182
+ lang:{
183
+ /*
184
+ * МОДУЛЬ ПЕРЕВОДОВ
185
+ *
186
+ * По слухам этот модуль лучше чем многие i18n реализации
187
+ * Всё потому что он из коробки умеет переводить страницу без перезагрузки
188
+ *
189
+ * !!!: parse() обрабатывает ключи из vars и подставляет их значения
190
+ * ваш +ключ+ становится значением, и это значение динамичное
191
+ * Так удобнее отображать динамичные данные на сайтах
192
+ * Например никнейм пользователя
193
+ */
117
194
  addr:'',
118
195
  vars:{},
119
196
  main:{},
120
197
 
121
- _:(i)=>` data-trans="${i}"`,
122
198
  load(name){
123
199
  return new Promise((resolve,reject)=>{
124
- _.http.req('GET',_.lang.addr+name+'.json',false,{'Cache-Control':'no-cache,no-store,max-age=0'})
200
+ _.http.req('GET',this.addr+name+'.json',false,{'Cache-Control':'no-cache,no-store,max-age=0'})
125
201
  .then(data=>resolve(data));
126
202
  })
203
+ // ???: убрать ли захардкоженое отключение кеша
127
204
  },
128
205
  parse:(packet,vars=_.lang.vars)=>
206
+ /*
207
+ * Регулярка ищет всё, что внутри плюсов, в JSON они редки
208
+ * Это позволяет без конфликтов подставлять переменные если они нашлись
209
+ *
210
+ * ???: переделать под общий синтаксис типа {var}
211
+ */
129
212
  packet.replace(/\+([^+]+)\+/g,(match,key)=>{
130
213
  let v=vars[key];
131
214
  return v!== undefined ? v : match;
132
215
  }),
133
216
  async replace(name){
134
- const p=await this.load(name);
135
- _.lang.main=JSON.parse(this.parse(p));
217
+ const p=await this.load(name); // await короче Promise.then
218
+ this.main=JSON.parse(this.parse(p)); // без замены языка нельзя начинать перевод
136
219
  for (let e of _.$.qa('[data-trans]')){
137
220
  let key=e.dataset.trans,
138
- text=_.lang.main[key] || `<code>_.lang.get('${key}')</code>`,
221
+ text=this.main[key] || `<code>lang.get('${key}')</code>`,
222
+ // ???: убрать обёртывание отсутсвующих ключей в <code />
139
223
  tag=e.tagName;
140
224
 
141
225
  if (tag=== 'IMG') e.src=text;
@@ -143,26 +227,57 @@ _.lang={
143
227
  e[e.type=== 'submit' ? 'value' :'placeholder']=text;
144
228
  else e.innerHTML=text;
145
229
  }
230
+ // возвращаем для последующей обработки пакета, например для сохранения в _.storage
146
231
  return p;
147
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}"`,
148
246
  from:i=>_.lang.main[i] || console.warn(`_.lang> ${i} is undefined`) || i,
149
247
 
150
- text: i=>_.lang._(i)+`>${_.lang.from(i)}<`,
151
- submit: i=>_.lang._(i)+`value="${_.lang.from(i)}">`,
152
- input: i=>_.lang._(i)+`placeholder="${_.lang.from(i)}">`,
153
- textarea: i=>_.lang._(i)+`placeholder="${_.lang.from(i)}"><`,
154
- img: i=>_.lang._(i)+`src="${_.lang.from(i)}"`,
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)}"`,
155
258
  win(i){
156
- let text=_.lang.from(i),
157
- dT=_.lang._(i);
259
+ let text=this.from(i),
260
+ dT=this.attr(i);
158
261
  if (text== null || text== ''){
159
262
  text=i;
160
263
  dT='';
161
264
  }
162
265
  return `${dT}>${text}<`;
163
266
  },
164
- };
165
- _.http={
267
+ },
268
+ http:{
269
+ /*
270
+ * HTTP-КЛИЕНТ
271
+ *
272
+ * Обычная обёртка нав XHR для быстрых запросов
273
+ * Использую XHR вместо fetch
274
+ * Мне нужен прогресс загрузки (fetch его не даёт)
275
+ * Да и вам тоже не помешает прогресс загрузки
276
+ *
277
+ * В defaultHeaders вы можете установить хедеры по умолчанию
278
+ * Как пример Authorization: 'your token'
279
+ * ???: добавить возможность игнорировать дефолтные хедеры
280
+ */
166
281
  defaultHeaders:{},
167
282
  req(method,url,data='',headers={},fileProgressElement=false){
168
283
  return new Promise((resolve,reject)=>{
@@ -170,10 +285,12 @@ _.http={
170
285
 
171
286
  xhr.open(method,url);
172
287
 
173
- let allHeaders={..._.http.defaultHeaders,...headers};
288
+ let allHeaders={...this.defaultHeaders,...headers};
174
289
  for (let header in allHeaders)
175
290
  xhr.setRequestHeader(header,allHeaders[header]);
176
291
 
292
+ // !!!: fileProgressElement ожидает <progress> элемент без min/max
293
+ // Потому что value от 0 до 1
177
294
  if (fileProgressElement)
178
295
  xhr.upload.onprogress=(e)=>{
179
296
  if (e.lengthComputable){
@@ -192,22 +309,36 @@ _.http={
192
309
  xhr.send(data);
193
310
  });
194
311
  },
195
- };
196
- _.$={
312
+ },
313
+ $:{
197
314
  D: document,
198
-
199
315
  id:(i)=>_.$.D.getElementById(i),
200
316
  q:(i,p=_.$.D)=>p.querySelector(i),
201
317
  qa:(i,p=_.$.D)=>p.querySelectorAll(i),
202
318
 
203
319
  on:(el,ev,fn,opts)=>el.addEventListener(ev,fn,opts),
204
320
  off:(el,ev,fn,opts)=>el.removeEventListener(ev,fn,opts),
205
- };
206
- _.html=(strs,...args)=>{
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
+ */
207
337
  let strF=[];
208
338
  for (let i=0; i < args.length; i++)
209
339
  strF.push(strs[i],args[i]);
210
340
  strF.push(strs[strs.length - 1]);
341
+ // убираем лишние пробелы, чтобы не плодить пустые текстовые ноды
211
342
  strF=strF.join('').trim().replace(/\s+/g,' ');
212
343
 
213
344
  const template=_.$.D.createElement('template');
@@ -217,8 +348,56 @@ _.html=(strs,...args)=>{
217
348
  if (content.children.length=== 1)
218
349
  return content.firstChild;
219
350
  return content;
220
- };
221
- _.storage=class{
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{
222
401
  constructor(strg,name){
223
402
  this._=strg;
224
403
  this.n=name;
@@ -229,51 +408,45 @@ _.storage=class{
229
408
  clear=()=>Object.keys(this._)
230
409
  .filter(k=>k.startsWith(this.n))
231
410
  .forEach(k=>this._.removeItem(k));
232
- };
233
- _.err={
234
- print:()=>{},
235
-
236
- errors:{},
237
- _c: 0,
238
- log(err){
239
- _.err.print(_.err._c,err);
240
- _.err._c++;
241
- _.err.errors[_.err._c]=err;
242
- },
243
- handleGlobal(message,source,line,column,error){
244
- console.error(message,source+':'+line+':'+column,error)
245
- _.err.log(message+`\n IN ${source} ON LINE ${line} IN COLUMN ${column}`);
246
- },
247
- handleRejection(e){
248
- const err=e.reason || e;
249
- console.error(err);
250
- _.err.log(
251
- `PROMISE ERROR\n`+
252
- `${e.stack || e}`
253
- );
254
- },
255
- };
256
- _.hotkeys={
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
+ */
257
428
  keys:{},
258
429
  _holds:new Set(),
259
430
  _:false,
260
431
 
261
432
  _parse:combo=>combo.split('+').map(k=>k.trim()),
262
433
  _match(keys) {
434
+ // Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
263
435
  for (let k of keys) if (!this._holds.has(k)) return false;
264
436
  return true;
265
437
  },
266
438
  _init() {
267
439
  if (this._) return;
268
440
  _.$.on(_.$.D,'keydown',e=>{
269
- this._holds.add(e.code);
441
+ this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
442
+ // code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
270
443
 
271
444
  for (let combo in this.keys) {
272
445
  let h=this.keys[combo];
273
446
  if (!this._match(h.keys)) continue;
274
447
 
275
448
  if (h.press && !h.active) {
276
- h.active=true;
449
+ h.active=true; // active защищает от множественных срабатываний
277
450
  h.press(e);
278
451
  }
279
452
  }
@@ -290,6 +463,10 @@ _.hotkeys={
290
463
  }
291
464
  });
292
465
  _.$.on(window,'blur',()=>{
466
+ /*
467
+ * При переключении в другое окно автоматического keyup не будет
468
+ * Поэтому сбрасываем всё принудительно, мало ли
469
+ */
293
470
  for (let combo in this.keys) {
294
471
  let h=this.keys[combo];
295
472
  if (h.active) {
@@ -307,6 +484,7 @@ _.hotkeys={
307
484
 
308
485
  this.keys[combo]={
309
486
  keys,
487
+ // press/releace по умолчанию пустышки для сокращения синаксиса
310
488
  press: press || (()=>{}),
311
489
  release: release || (()=>{}),
312
490
  active: false
@@ -317,9 +495,33 @@ _.hotkeys={
317
495
  off(combo) {
318
496
  delete this.keys[combo];
319
497
  return this;
320
- }
321
- };
322
- _.win={
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
+ */
323
525
  manager:false,
324
526
  hider:false,
325
527
 
@@ -331,9 +533,9 @@ _.win={
331
533
  hiderAttrs:'',
332
534
 
333
535
  defBtns:[
334
- ['–',w=>_.win.hide(w)],
335
- ['=',w=>_.win.toggleFull(w)],
336
- ['X',w=>_.win.close(w)],
536
+ ['–',w=>w.hide()],
537
+ ['=',w=>w.toggleFull()],
538
+ ['X',w=>w.close()],
337
539
  ],
338
540
 
339
541
  animOpen:'',
@@ -342,46 +544,54 @@ _.win={
342
544
  animShow:'',
343
545
  animFullOn:'',
344
546
  animFullOff:'',
547
+ _ae:{once:true},
345
548
 
346
549
  _ID(){
347
550
  let id;
551
+ // Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
552
+ // !!!: в теории можно задать любой айди
553
+ // ???: проверить при скольки окнах генератор начинает тормозить
348
554
  do id=Math.random().toString(36).substring(2,8);
349
555
  while (_.wins[id]);
350
556
  return id;
351
557
  },
352
558
  _winBtn(win,text,func){
353
- let b=_.html`<button ${_.win.btnAttrs}>${text}</button>`;
559
+ let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
354
560
  _.$.on(b,'click',()=>func(win));
355
561
  return b;
356
562
  },
357
563
  _hiderBtn(win){
358
564
  let title=win.langs!== false ? _.lang.win('WINDOW-'+win.langs) : `>${win.name}<`,
359
- b=_.html`<button id=hider${win.id} ${_.win.hiderAttrs}${title}/button>`;
360
- _.$.on(b,'click',()=>_.win.show(win));
565
+ b=_.html`<button id=hider${win.id} ${this.hiderAttrs}${title}/button>`;
566
+ _.$.on(b,'click',()=>this.show(win));
361
567
  return b;
362
568
  },
363
569
  _initWin(win){
364
- let wEl=win.elem,
365
- x1=0,y1=0,x2=0,y2=0,
570
+ let D=_.$.D,
571
+ wEl=win.elem,
572
+ x1=0,y1=0,x2=0,y2=0,
573
+ prevent=e=>e.preventDefault(),
366
574
  startW=e=>{
367
575
  let targ=e.target;
576
+ // Проверяем куда нажали, если бы мы не проверяли,
577
+ // То драггер не дал бы нам нажать на кнопки или изменить имя окна
368
578
  if (['BUTTON','INPUT'].includes(targ.tagName) || targ.closest('button,input')){
369
579
  return;
370
580
  }
371
- _.win.manager.appendChild(wEl);
581
+ this.manager.appendChild(wEl);
372
582
 
373
- e.preventDefault();
374
- x2=e.clientX || e.touches[0].clientX;
375
- y2=e.clientY || e.touches[0].clientY;
583
+ prevent(e);
584
+ x2=e.clientX;
585
+ y2=e.clientY;
376
586
 
377
- _.$.D.onmouseup=_.$.D.ontouchend=stopW;
587
+ D.onpointermove=moveW;
378
588
 
379
- _.$.D.onmousemove=_.$.D.ontouchmove=moveW;
589
+ D.onpointerup=D.onpointercancel=stopW;
380
590
  },
381
591
  moveW=e=>{
382
- e.preventDefault();
383
- let cX=e.clientX || e.touches[0].clientX,
384
- cY=e.clientY || e.touches[0].clientY;
592
+ prevent(e);
593
+ let cX=e.clientX,
594
+ cY=e.clientY;
385
595
 
386
596
  x1=x2 - cX;
387
597
  y1=y2 - cY;
@@ -391,83 +601,97 @@ _.win={
391
601
  wEl.style.top=(wEl.offsetTop - y1) + "px";
392
602
  wEl.style.left=(wEl.offsetLeft - x1) + "px";
393
603
  },
394
- stopW=()=>{
395
- ['mouseup','touchend','mousemove','touchmove']
396
- .forEach(e=>_.$.D['on'+e]=null)
397
- },
398
- drag=win.drag;
399
- drag.onmousedown=drag.ontouchstart=startW;
604
+ stopW=()=>['move','up','cancel'].map(e=>D['onpointer'+e]=null),
605
+ dr=win.drag;
606
+ dr.onpointerdown=startW;
607
+ dr.ontouchmove=prevent;
400
608
  },
401
609
  open(name,content='',customAttrs=''){
402
- if (!_.win.manager || !_.win.hider)
403
- throw new Error('Window managers not inited');
404
- let winId=_.win._ID();
405
- _.wins[winId]={};
406
- let s=_.wins[winId];
407
- s.id=winId;
408
- s.name=name;
409
- s.langs=name;
410
- s.state='opened';
411
- s.full=false;
412
- s.inRename=false;
413
- s.onUnfull={top:0,left:0,width:0,height:0,};
414
-
415
- s.setTitle = newTitle=>_.win.setTitle(s,newTitle);
416
- s.toggleFull = e=>_.win.toggleFull(s);
417
- s.close = e=>_.win.close(s);
418
- s.hide = e=>_.win.hide(s);
419
- s.show = e=>_.win.show(s);
420
-
421
- let html =
422
- _.html`<div id=${winId} ${_.win.winAttrs} ${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}>
423
634
  <div style="display:flex;justify-content:space-between;align-items:center"
424
- ${_.win.dragAttrs} id=DRAGGER${winId}>
425
- <span ${_.win.titleAttrs} id=title${winId}${_.lang.win('WINDOW-'+name)}/span>
426
- <div id=btns${winId}></div>
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>
427
638
  </div>
428
- <div id=content${winId} style=overflow:auto;width:100%;height:100%>
429
- ${content.replace(/\{winId\}/g,winId)}
639
+ <div id=content${wId} style=overflow:auto;width:100%;height:100%>
640
+ ${content.replace(/\{winId\}/g,wId)}
430
641
  </div>
431
642
  </div>`,
432
- btns=_.$.q(`#btns${winId}`,html);
433
- for(let b of _.win.defBtns) btns.append(_.win._winBtn(s,...b));
434
-
643
+ btns=_.$.q(`#btns${wId}`,html);
644
+ for(let b of this.defBtns) btns.append(this._winBtn(w,...b));
435
645
  html.style.overflow='hidden';
436
646
  html.style.resize='both';
437
647
 
438
- let anim=_.win.animOpen;
648
+ let anim=this.animOpen;
439
649
  if (anim)
440
- _.$.on(html,'animationend',()=>html.classList.remove(anim),{ once: true })
441
- _.win.manager.append(html);
442
-
443
- let win=s.elem=_.$.id(winId),
444
- c=_.$.id('content'+winId).getBoundingClientRect(), r=win.getBoundingClientRect(),
445
- padX=r.width - c.width, padY=r.height - c.height;
446
- s.drag=_.$.id('DRAGGER'+winId);
447
- s.content=_.$.id('content'+winId);
448
-
449
- if (!customAttrs.includes('top')) {
450
- win.style.top=win.offsetTop - (win.offsetHeight / 2) + 'px';
451
- win.style.left=win.offsetLeft - (win.offsetWidth / 2) + 'px';
452
- }
453
- if (!customAttrs.includes('width')) win.style.height=(win.offsetHeight - padX) + 'px';
454
- if (!customAttrs.includes('height')) win.style.width=(win.offsetWidth - padY) + 'px';
455
-
456
- _.$.on(s.drag,'dblclick',(e)=>{
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();
457
681
  if(e.target.closest('button')) return;
458
- let wT=_.$.id('title'+winId);
459
- if (!s.inRename){
460
- wT.innerHTML=`<input ${_.win.renameAttrs} id=rename${winId} value="${wT.textContent}">`;
461
- s.inRename=true;
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;
462
686
  }else{
463
- _.win.setTitle(s,_.$.id('rename'+winId).value);
464
- s.inRename=false;
687
+ this.setTitle(w,_.$.id('rename'+wId).value);
688
+ w.inRename=false;
465
689
  }
466
690
  });
467
691
 
468
- _.win._initWin(s);
692
+ if (w.state === 'hidened') w.hide();
469
693
 
470
- return s;
694
+ return w;
471
695
  },
472
696
  setTitle(win,newT){
473
697
  win.langs=false;
@@ -485,12 +709,12 @@ _.win={
485
709
  let wEl=win.elem,
486
710
  ws=wEl.style,
487
711
  wc=wEl.classList,
488
- cont=_.$.id('content'+win.id).getBoundingClientRect(),
489
- rect=wEl.getBoundingClientRect(),
712
+ cont=_.$.cliRect(_.$.id('content'+win.id)),
713
+ rect=_.$.cliRect(wEl),
490
714
  padX=rect.width - cont.width,
491
715
  padY=rect.height - cont.height,
492
- aOn=_.win.animFullOn,
493
- aOff=_.win.animFullOff,
716
+ aOn=this.animFullOn,
717
+ aOff=this.animFullOff,
494
718
  fd={
495
719
  top: rect.top, left: rect.left,
496
720
  width: cont.width, height: cont.height,
@@ -509,35 +733,37 @@ _.win={
509
733
  ws.left=0;
510
734
  ws.width=`calc(100% - ${padX}px)`;
511
735
  ws.height=`calc(100% - ${padY}px)`;
512
- win.drag.onmousedown=null;
513
- win.drag.ontouchstart=null;
736
+ win.drag.onpointerdown=null;
514
737
  },
515
738
  doUnful=()=>{
516
739
  if (aOff) wc.remove(aOff);
517
740
  unful();
518
741
  win.full=false;
519
- _.win._initWin(win);
742
+ this._initWin(win);
520
743
  },
521
744
  old=win.onUnfull;
522
745
  if (!win.full) {
523
746
  if (aOn) {
524
747
  wc.add(aOn);
525
- _.$.on(wEl,'animationend',doFul,{ once: true });
748
+ _.$.on(wEl,'animationend',doFul,this._ae);
526
749
  }else doFul();
527
750
  } else {
528
751
  if (aOff) {
529
752
  wc.add(aOff);
530
753
  unful();
531
- _.$.on(wEl,'animationend',doUnful,{ once: true });
754
+ _.$.on(wEl,'animationend',doUnful,this._ae);
532
755
  }else doUnful();
533
756
  }
534
757
  },
535
758
  close(win){
536
759
  let w=win.elem,
537
760
  remover=()=>{
538
- let drag=win.drag;
539
- drag.onmousedown=null;
540
- drag.ontouchstart=null;
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);
541
767
  w.remove();
542
768
  delete _.wins[win.id];
543
769
  };
@@ -545,34 +771,34 @@ _.win={
545
771
  _.$.id('hider'+win.id).remove();
546
772
  remover();
547
773
  }else{
548
- let anim=_.win.animClose;
774
+ let anim=this.animClose;
549
775
  if(anim){
550
776
  w.classList.add(anim);
551
- _.$.on(w,'animationend',remover,{ once: true });
777
+ _.$.on(w,'animationend',remover,this._ae);
552
778
  }else
553
779
  remover();
554
780
  }
555
781
  },
556
782
  hide(win){
557
- let wEl = win.elem,
783
+ let wEl=win.elem,
558
784
  wc=wEl.classList,
559
- anim=_.win.animHide,
785
+ anim=this.animHide,
560
786
  hider=()=>{
561
787
  wEl.style.display='none';
562
788
  if(anim)wc.remove(anim);
563
789
  win.state='hidened';
564
- _.win.hider.append(_.win._hiderBtn(win));
790
+ this.hider.append(this._hiderBtn(win));
565
791
  }
566
792
  if(anim){
567
793
  wc.add(anim);
568
- _.$.on(wEl,'animationend',hider,{ once: true });
794
+ _.$.on(wEl,'animationend',hider,this._ae);
569
795
  }else
570
796
  hider();
571
797
  },
572
798
  show(win){
573
- let wEl = win.elem,
799
+ let wEl=win.elem,
574
800
  wc=wEl.classList,
575
- anim=_.win.animShow,
801
+ anim=this.animShow,
576
802
  hider=_.$.id('hider'+win.id),
577
803
  shower=()=>{
578
804
  if(anim)wc.remove(anim);
@@ -582,22 +808,63 @@ _.win={
582
808
  hider.remove();
583
809
  if(anim){
584
810
  wc.add(anim);
585
- _.$.on(wEl,'animationend',shower,{ once: true });
811
+ _.$.on(wEl,'animationend',shower,this._ae);
586
812
  }else
587
813
  shower()
588
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:{},
589
845
  };
590
- _.wins={};
591
846
 
592
- _.$.on(window,'error',_.err.handleGlobal);
593
- _.$.on(window,'unhandledrejection',_.err.handleRejection);
594
847
  _.$.on(window,'popstate',()=>{
595
848
  let l=_.link
849
+ /*
850
+ * popstate срабатывает когда:
851
+ * - пользователь прыгает по истории назад/вперёд
852
+ * - мы вызываем history.pushState (не replaceState)
853
+ *
854
+ * _i различает эти случаи:
855
+ * true = пользователь прыгнул назад
856
+ * false = страница пишет свой адрес в ссылку
857
+ */
858
+ // ???: некоторые браузеры могут вызывать popstate и при реплейсе
596
859
  if (!l._i) {
860
+ // здесь происходит перенос команд при popstate
861
+ // читайте _.lang.get() если хотите узнать почему
597
862
  let nUrl='?' + [l.compile()[0],...l._cmd].join('&');
598
- history.replaceState(null,null,nUrl);
599
863
  l._i=true;
864
+ history.replaceState(null,null,nUrl);
600
865
  l.get();
601
866
  } else
602
867
  l._i=false;
603
868
  });
869
+
870
+ return _}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newhelper-js",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "сверх лёгкая библиотека для построения админок",
5
5
  "keywords": [
6
6
  "admin",