newhelper-js 2.1.1 → 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.
- package/README-en.md +1 -1
- package/README.md +2 -2
- package/docs.md +1 -1
- package/newHelper.js +801 -362
- package/package.json +1 -1
package/newHelper.js
CHANGED
|
@@ -1,327 +1,720 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
17
56
|
history.pushState(null,null,'?'+link.join('&'));
|
|
18
57
|
}
|
|
19
|
-
this._i=false;
|
|
58
|
+
this._i = false;
|
|
20
59
|
},
|
|
21
|
-
add(cmd){
|
|
22
|
-
let link=this.compile();
|
|
23
|
-
if (!link.includes(cmd)){
|
|
60
|
+
add(cmd) {
|
|
61
|
+
let link = this.compile();
|
|
62
|
+
if (!link.includes(cmd)) {
|
|
24
63
|
link.push(cmd);
|
|
25
64
|
this._cmd.push(cmd);
|
|
26
65
|
history.replaceState(null,null,'?'+link.join('&'));
|
|
27
66
|
}
|
|
28
67
|
},
|
|
29
|
-
remove(cmd){
|
|
30
|
-
let link=this.compile();
|
|
68
|
+
remove(cmd) {
|
|
69
|
+
let link = this.compile();
|
|
31
70
|
if (link.includes(cmd)){
|
|
32
|
-
let c=this._cmd;
|
|
71
|
+
let c = this._cmd;
|
|
33
72
|
link.splice(link.indexOf(cmd),1);
|
|
34
73
|
c.splice(c.indexOf(cmd),1);
|
|
35
74
|
history.replaceState(null,null,'?'+link.join('&'));
|
|
36
75
|
}
|
|
37
76
|
},
|
|
38
|
-
get(){
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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);
|
|
43
89
|
try {
|
|
44
|
-
let dirs=
|
|
45
|
-
dir=
|
|
46
|
-
main=dir[
|
|
47
|
-
if (!
|
|
48
|
-
main(
|
|
90
|
+
let dirs = firstKey.split('/'),
|
|
91
|
+
dir = this.actions,
|
|
92
|
+
main = dir[firstKey];
|
|
93
|
+
if (!firstKey.includes('/')) {
|
|
94
|
+
main(fisrtValue);
|
|
49
95
|
} else {
|
|
50
96
|
for (let p of dirs){
|
|
51
|
-
let kDir=dir[p+'/'];
|
|
97
|
+
let kDir = dir[p+'/'];
|
|
52
98
|
if (kDir)
|
|
53
|
-
dir=kDir;
|
|
99
|
+
dir = kDir;
|
|
54
100
|
else{
|
|
55
|
-
dir[p](
|
|
101
|
+
dir[p](fisrtValue);
|
|
56
102
|
break;
|
|
57
103
|
}
|
|
58
104
|
}
|
|
59
105
|
}
|
|
60
|
-
}catch(e){
|
|
61
|
-
|
|
106
|
+
} catch (e) {
|
|
107
|
+
this.basePage();
|
|
62
108
|
throw e;
|
|
63
109
|
}
|
|
64
|
-
|
|
65
|
-
cmds.forEach(
|
|
66
|
-
let [key,
|
|
67
|
-
let cmd=
|
|
110
|
+
this._cmd = cmds;
|
|
111
|
+
cmds.forEach(cmdPre => {
|
|
112
|
+
let [ key, value ] = cmdPre.split('=');
|
|
113
|
+
let cmd = this.commands[key];
|
|
68
114
|
if (cmd)
|
|
69
|
-
cmd(
|
|
115
|
+
cmd(value);
|
|
116
|
+
else
|
|
117
|
+
console.error(new Error(`command '${cmd}' doesn't exist!`))
|
|
70
118
|
});
|
|
71
119
|
},
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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;
|
|
88
160
|
resolve(args);
|
|
89
161
|
};
|
|
90
|
-
scr.onerror=()=>{
|
|
162
|
+
scr.onerror = ()=>{
|
|
91
163
|
delete state[key];
|
|
92
164
|
reject(new Error('Failed to load '+url));
|
|
93
165
|
};
|
|
94
|
-
|
|
166
|
+
document.head.append(scr);
|
|
95
167
|
});
|
|
96
|
-
state[key]=
|
|
97
|
-
return
|
|
168
|
+
state[key] = promise;
|
|
169
|
+
return promise;
|
|
98
170
|
},
|
|
99
|
-
register(
|
|
100
|
-
if (!Array.isArray(funcs))
|
|
101
|
-
|
|
102
|
-
for (let fn of funcs)
|
|
103
|
-
window[fn]=(...a)=>_.lazy._(scr,fn).then(f=>f(...a));
|
|
171
|
+
register(script, funcs) {
|
|
172
|
+
if (!Array.isArray(funcs))
|
|
173
|
+
return new Error('Array required for register');
|
|
104
174
|
|
|
105
|
-
|
|
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
|
+
}
|
|
106
183
|
},
|
|
107
|
-
async
|
|
108
|
-
let w=window,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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];
|
|
113
192
|
throw new Error(`Function ${fn} not loaded from ${scr}`);
|
|
114
193
|
},
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
217
|
}),
|
|
133
218
|
async replace(name){
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
229
|
else if (['INPUT','TEXTAREA'].includes(tag))
|
|
143
|
-
|
|
144
|
-
else
|
|
230
|
+
el[ el.type === 'submit' ? 'value' : 'placeholder' ] = text;
|
|
231
|
+
else
|
|
232
|
+
el.innerHTML = text;
|
|
145
233
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 = '';
|
|
161
263
|
}
|
|
162
|
-
return `${
|
|
264
|
+
return `${dataTrans}>${text}<`;
|
|
163
265
|
},
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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();
|
|
170
284
|
|
|
171
|
-
xhr.open(method,url);
|
|
285
|
+
xhr.open(method, url);
|
|
172
286
|
|
|
173
|
-
let allHeaders={...
|
|
287
|
+
let allHeaders = { ...this.defaultHeaders, ...headers };
|
|
174
288
|
for (let header in allHeaders)
|
|
175
|
-
xhr.setRequestHeader(header,allHeaders[header]);
|
|
289
|
+
xhr.setRequestHeader(header, allHeaders[header]);
|
|
176
290
|
|
|
291
|
+
// !!!: fileProgressElement ожидает <progress> элемент без min/max
|
|
292
|
+
// Потому что value от 0 до 1
|
|
177
293
|
if (fileProgressElement)
|
|
178
|
-
xhr.upload.onprogress=
|
|
179
|
-
if (e.lengthComputable){
|
|
180
|
-
let percentage=(e.loaded / e.total);
|
|
181
|
-
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);
|
|
182
298
|
}
|
|
183
299
|
};
|
|
184
300
|
|
|
185
|
-
xhr.onreadystatechange=()=>{
|
|
301
|
+
xhr.onreadystatechange= ()=>{
|
|
186
302
|
if (xhr.readyState=== 4)
|
|
187
|
-
if (xhr.status >= 200 && xhr.status < 300)
|
|
188
|
-
|
|
303
|
+
if (xhr.status >= 200 && xhr.status < 300)
|
|
304
|
+
resolve(xhr.response);
|
|
305
|
+
else
|
|
306
|
+
reject(new Error(`${xhr.status} - ${xhr.statusText}`),xhr);
|
|
189
307
|
};
|
|
190
|
-
xhr.onerror=()=>
|
|
308
|
+
xhr.onerror = ()=>
|
|
309
|
+
reject(new Error('Network error'), xhr);
|
|
191
310
|
|
|
192
311
|
xhr.send(data);
|
|
193
312
|
});
|
|
194
313
|
},
|
|
195
|
-
}
|
|
196
|
-
_
|
|
197
|
-
|
|
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];
|
|
198
345
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
346
|
+
const template = document.createElement('template');
|
|
347
|
+
template.innerHTML = fullStr;
|
|
348
|
+
const content = template.content;
|
|
202
349
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const template=_.$.D.createElement('template');
|
|
214
|
-
template.innerHTML=strF;
|
|
215
|
-
|
|
216
|
-
const content=template.content;
|
|
217
|
-
if (content.children.length=== 1)
|
|
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)
|
|
218
360
|
return content.firstChild;
|
|
219
361
|
return content;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
225
384
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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>`;
|
|
235
478
|
|
|
236
|
-
|
|
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: {},
|
|
237
535
|
_c: 0,
|
|
238
|
-
log(err){
|
|
536
|
+
log(err) {
|
|
239
537
|
_.err.print(_.err._c,err);
|
|
240
538
|
_.err._c++;
|
|
241
539
|
_.err.errors[_.err._c]=err;
|
|
242
540
|
},
|
|
243
541
|
handleGlobal(message,source,line,column,error){
|
|
244
542
|
console.error(message,source+':'+line+':'+column,error)
|
|
245
|
-
_.err.log(message
|
|
543
|
+
_.err.log(message + `\n IN ${source} ON LINE ${line} IN COLUMN ${column}`);
|
|
246
544
|
},
|
|
247
545
|
handleRejection(e){
|
|
248
|
-
const err=e.reason || e;
|
|
546
|
+
const err = e.reason || e;
|
|
249
547
|
console.error(err);
|
|
250
548
|
_.err.log(
|
|
251
549
|
`PROMISE ERROR\n`+
|
|
252
550
|
`${e.stack || e}`
|
|
253
551
|
);
|
|
254
552
|
},
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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,
|
|
260
573
|
|
|
261
|
-
_parse:combo=>combo.split('+').map(k=>k.trim()),
|
|
574
|
+
_parse: combo => combo.split('+').map(k=>k.trim()),
|
|
262
575
|
_match(keys) {
|
|
576
|
+
// Нужно сверять все клавишы, это же КОМБИНАЦИЯ а не отдельные куски
|
|
263
577
|
for (let k of keys) if (!this._holds.has(k)) return false;
|
|
264
578
|
return true;
|
|
265
579
|
},
|
|
266
580
|
_init() {
|
|
267
|
-
if (this._)
|
|
268
|
-
|
|
269
|
-
|
|
581
|
+
if (this._)
|
|
582
|
+
return;
|
|
583
|
+
document.addEventListener('keydown', e=>{
|
|
584
|
+
this._holds.add(e.code);// key зависит от раскладки (на Qwerty 'KeyZ' — это 'z', на Йцукен — 'я')
|
|
585
|
+
// code даёт физическое положение клавиши, что важно для игр и хоткеев, и в целом универсальнее
|
|
270
586
|
|
|
271
|
-
for (let
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
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);
|
|
278
593
|
}
|
|
279
594
|
}
|
|
280
595
|
});
|
|
281
|
-
|
|
596
|
+
document.addEventListener('keyup', e=>{
|
|
282
597
|
this._holds.delete(e.code);
|
|
283
598
|
|
|
284
|
-
for (let
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
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);
|
|
289
603
|
}
|
|
290
604
|
}
|
|
291
605
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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();
|
|
298
615
|
}
|
|
299
616
|
}
|
|
300
617
|
this._holds.clear();
|
|
301
618
|
});
|
|
302
619
|
this._=true;
|
|
303
620
|
},
|
|
304
|
-
on(combo,press,release) {
|
|
621
|
+
on(combo, press, release) {
|
|
305
622
|
this._init();
|
|
306
|
-
let keys=this._parse(combo);
|
|
623
|
+
let keys = this._parse(combo);
|
|
307
624
|
|
|
308
|
-
this.keys
|
|
625
|
+
this.keys.set(combo, {
|
|
309
626
|
keys,
|
|
627
|
+
// press/releace по умолчанию пустышки для сокращения синаксиса
|
|
310
628
|
press: press || (()=>{}),
|
|
311
629
|
release: release || (()=>{}),
|
|
312
630
|
active: false
|
|
313
|
-
};
|
|
631
|
+
});
|
|
314
632
|
|
|
315
633
|
return this;
|
|
316
634
|
},
|
|
317
635
|
off(combo) {
|
|
318
|
-
|
|
636
|
+
this.keys.delete(combo);
|
|
319
637
|
return this;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
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
|
+
*/
|
|
323
715
|
manager:false,
|
|
324
716
|
hider:false,
|
|
717
|
+
text:'',
|
|
325
718
|
|
|
326
719
|
winAttrs:'',
|
|
327
720
|
dragAttrs:'',
|
|
@@ -331,9 +724,9 @@ _.win={
|
|
|
331
724
|
hiderAttrs:'',
|
|
332
725
|
|
|
333
726
|
defBtns:[
|
|
334
|
-
['–',w=>
|
|
335
|
-
['=',w=>
|
|
336
|
-
['X',w=>
|
|
727
|
+
['–',w=>w.hide()],
|
|
728
|
+
['=',w=>w.toggleFull()],
|
|
729
|
+
['X',w=>w.close()],
|
|
337
730
|
],
|
|
338
731
|
|
|
339
732
|
animOpen:'',
|
|
@@ -342,138 +735,125 @@ _.win={
|
|
|
342
735
|
animShow:'',
|
|
343
736
|
animFullOn:'',
|
|
344
737
|
animFullOff:'',
|
|
738
|
+
_ae:{once:true},
|
|
345
739
|
|
|
346
740
|
_ID(){
|
|
347
741
|
let id;
|
|
742
|
+
// Создаём случайный 6 символьный айди, чтобы каждый раз не совпадало
|
|
743
|
+
// !!!: в теории можно задать любой айди
|
|
744
|
+
// ???: проверить при скольки окнах генератор начинает тормозить
|
|
348
745
|
do id=Math.random().toString(36).substring(2,8);
|
|
349
|
-
while (_.wins
|
|
746
|
+
while (_.wins.has(id));
|
|
350
747
|
return id;
|
|
351
748
|
},
|
|
352
749
|
_winBtn(win,text,func){
|
|
353
|
-
let b=_.html`<button ${
|
|
354
|
-
|
|
750
|
+
let b=_.html`<button ${this.btnAttrs}>${text}</button>`;
|
|
751
|
+
b.addEventListener('click',()=>func(win));
|
|
355
752
|
return b;
|
|
356
753
|
},
|
|
357
754
|
_hiderBtn(win){
|
|
358
|
-
let title=win.langs!== false ? _.lang.win
|
|
359
|
-
b=_.html`<button id=hider${win.id} ${
|
|
360
|
-
|
|
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));
|
|
361
758
|
return b;
|
|
362
759
|
},
|
|
363
|
-
_initWin
|
|
364
|
-
|
|
365
|
-
x1=0,y1=0,x2=0,y2=0,
|
|
366
|
-
startW=e=>{
|
|
367
|
-
let targ=e.target;
|
|
368
|
-
if (['BUTTON','INPUT'].includes(targ.tagName) || targ.closest('button,input')){
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
_.win.manager.appendChild(wEl);
|
|
372
|
-
|
|
373
|
-
e.preventDefault();
|
|
374
|
-
x2=e.clientX || e.touches[0].clientX;
|
|
375
|
-
y2=e.clientY || e.touches[0].clientY;
|
|
376
|
-
|
|
377
|
-
_.$.D.onmouseup=_.$.D.ontouchend=stopW;
|
|
378
|
-
|
|
379
|
-
_.$.D.onmousemove=_.$.D.ontouchmove=moveW;
|
|
380
|
-
},
|
|
381
|
-
moveW=e=>{
|
|
382
|
-
e.preventDefault();
|
|
383
|
-
let cX=e.clientX || e.touches[0].clientX,
|
|
384
|
-
cY=e.clientY || e.touches[0].clientY;
|
|
385
|
-
|
|
386
|
-
x1=x2 - cX;
|
|
387
|
-
y1=y2 - cY;
|
|
388
|
-
x2=cX;
|
|
389
|
-
y2=cY;
|
|
390
|
-
|
|
391
|
-
wEl.style.top=(wEl.offsetTop - y1) + "px";
|
|
392
|
-
wEl.style.left=(wEl.offsetLeft - x1) + "px";
|
|
393
|
-
},
|
|
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;
|
|
400
|
-
},
|
|
760
|
+
_initWin: winState=>
|
|
761
|
+
_.drag.init(winState.drag, winState.elem, ()=>_.win.manager.appendChild(winState.elem)),
|
|
401
762
|
open(name,content='',customAttrs=''){
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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}>
|
|
423
787
|
<div style="display:flex;justify-content:space-between;align-items:center"
|
|
424
|
-
${
|
|
425
|
-
<span ${
|
|
426
|
-
<div id=btns${
|
|
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>
|
|
427
791
|
</div>
|
|
428
|
-
<div id=content${
|
|
429
|
-
${content.replace(/\{winId\}/g,
|
|
792
|
+
<div id=content${wId} style=overflow:auto;width:100%;height:100%>
|
|
793
|
+
${content.replace(/\{winId\}/g,wId)}
|
|
430
794
|
</div>
|
|
431
795
|
</div>`,
|
|
432
|
-
btns=
|
|
433
|
-
for(let b of
|
|
434
|
-
|
|
796
|
+
btns=html.querySelector(`#btns${wId}`);
|
|
797
|
+
for(let b of this.defBtns) btns.append(this._winBtn(winState,...b));
|
|
435
798
|
html.style.overflow='hidden';
|
|
436
799
|
html.style.resize='both';
|
|
437
800
|
|
|
438
|
-
let anim=
|
|
801
|
+
let anim=this.animOpen;
|
|
439
802
|
if (anim)
|
|
440
|
-
|
|
441
|
-
_.win.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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';
|
|
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);
|
|
455
810
|
|
|
456
|
-
|
|
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();
|
|
457
836
|
if(e.target.closest('button')) return;
|
|
458
|
-
let wT=
|
|
459
|
-
if (!
|
|
460
|
-
wT.innerHTML=`<input ${
|
|
461
|
-
|
|
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;
|
|
462
841
|
}else{
|
|
463
|
-
|
|
464
|
-
|
|
842
|
+
this.setTitle(winState,document.getElementById('rename'+wId).value);
|
|
843
|
+
winState.inRename=false;
|
|
465
844
|
}
|
|
466
845
|
});
|
|
467
846
|
|
|
468
|
-
|
|
847
|
+
if (winState.state === 'hidened') winState.hide();
|
|
469
848
|
|
|
470
|
-
|
|
849
|
+
_.wins.set(winState.id, winState);
|
|
850
|
+
return winState;
|
|
471
851
|
},
|
|
472
|
-
setTitle(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
let t=
|
|
476
|
-
h=
|
|
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);
|
|
477
857
|
t.innerHTML=newT;
|
|
478
858
|
t.removeAttribute('data-trans');
|
|
479
859
|
if (h){
|
|
@@ -481,19 +861,19 @@ _.win={
|
|
|
481
861
|
h.removeAttribute('data-trans');
|
|
482
862
|
}
|
|
483
863
|
},
|
|
484
|
-
toggleFull(
|
|
485
|
-
let wEl=
|
|
864
|
+
toggleFull(winState){
|
|
865
|
+
let wEl=winState.elem,
|
|
486
866
|
ws=wEl.style,
|
|
487
867
|
wc=wEl.classList,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
padX=
|
|
491
|
-
padY=
|
|
492
|
-
aOn=
|
|
493
|
-
aOff=
|
|
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,
|
|
494
874
|
fd={
|
|
495
|
-
top:
|
|
496
|
-
width:
|
|
875
|
+
top: windowRect.top, left: windowRect.left,
|
|
876
|
+
width: contentRect.width, height: contentRect.height,
|
|
497
877
|
},
|
|
498
878
|
unful=()=>{
|
|
499
879
|
ws.top=old.top + 'px';
|
|
@@ -503,101 +883,160 @@ _.win={
|
|
|
503
883
|
},
|
|
504
884
|
doFul=()=>{
|
|
505
885
|
if (aOn) wc.remove(aOn);
|
|
506
|
-
|
|
507
|
-
|
|
886
|
+
winState.full=true;
|
|
887
|
+
winState.onUnfull=fd;
|
|
508
888
|
ws.top=0;
|
|
509
889
|
ws.left=0;
|
|
510
890
|
ws.width=`calc(100% - ${padX}px)`;
|
|
511
891
|
ws.height=`calc(100% - ${padY}px)`;
|
|
512
|
-
|
|
513
|
-
win.drag.ontouchstart=null;
|
|
892
|
+
winState.drag.onpointerdown=null;
|
|
514
893
|
},
|
|
515
894
|
doUnful=()=>{
|
|
516
895
|
if (aOff) wc.remove(aOff);
|
|
517
896
|
unful();
|
|
518
|
-
|
|
519
|
-
|
|
897
|
+
winState.full=false;
|
|
898
|
+
this._initWin(winState);
|
|
520
899
|
},
|
|
521
|
-
old=
|
|
522
|
-
if (!
|
|
900
|
+
old=winState.onUnfull;
|
|
901
|
+
if (!winState.full) {
|
|
523
902
|
if (aOn) {
|
|
524
903
|
wc.add(aOn);
|
|
525
|
-
|
|
904
|
+
wEl.addEventListener('animationend',doFul,this._ae);
|
|
526
905
|
}else doFul();
|
|
527
906
|
} else {
|
|
528
907
|
if (aOff) {
|
|
529
908
|
wc.add(aOff);
|
|
530
909
|
unful();
|
|
531
|
-
|
|
910
|
+
wEl.addEventListener('animationend',doUnful,this._ae);
|
|
532
911
|
}else doUnful();
|
|
533
912
|
}
|
|
534
913
|
},
|
|
535
|
-
close(
|
|
536
|
-
let w=
|
|
914
|
+
close(winState){
|
|
915
|
+
let w=winState.elem,
|
|
537
916
|
remover=()=>{
|
|
538
|
-
let
|
|
539
|
-
|
|
540
|
-
|
|
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);
|
|
541
923
|
w.remove();
|
|
542
|
-
|
|
924
|
+
_.wins.delete(winState.id);
|
|
543
925
|
};
|
|
544
926
|
if (w.style.display== 'none'){
|
|
545
|
-
|
|
927
|
+
document.getElementById('hider'+winState.id).remove();
|
|
546
928
|
remover();
|
|
547
929
|
}else{
|
|
548
|
-
let anim=
|
|
930
|
+
let anim=this.animClose;
|
|
549
931
|
if(anim){
|
|
550
932
|
w.classList.add(anim);
|
|
551
|
-
|
|
933
|
+
w.addEventListener('animationend',remover,this._ae);
|
|
552
934
|
}else
|
|
553
935
|
remover();
|
|
554
936
|
}
|
|
555
937
|
},
|
|
556
|
-
hide(
|
|
557
|
-
let wEl
|
|
938
|
+
hide(winState){
|
|
939
|
+
let wEl=winState.elem,
|
|
558
940
|
wc=wEl.classList,
|
|
559
|
-
anim=
|
|
941
|
+
anim=this.animHide,
|
|
560
942
|
hider=()=>{
|
|
561
943
|
wEl.style.display='none';
|
|
562
944
|
if(anim)wc.remove(anim);
|
|
563
|
-
|
|
564
|
-
|
|
945
|
+
winState.state='hidened';
|
|
946
|
+
this.hider.append(this._hiderBtn(winState));
|
|
565
947
|
}
|
|
566
948
|
if(anim){
|
|
567
949
|
wc.add(anim);
|
|
568
|
-
|
|
950
|
+
wEl.addEventListener('animationend',hider,this._ae);
|
|
569
951
|
}else
|
|
570
952
|
hider();
|
|
571
953
|
},
|
|
572
|
-
show(
|
|
573
|
-
let wEl
|
|
954
|
+
show(winState){
|
|
955
|
+
let wEl=winState.elem,
|
|
574
956
|
wc=wEl.classList,
|
|
575
|
-
anim=
|
|
576
|
-
hider=
|
|
957
|
+
anim=this.animShow,
|
|
958
|
+
hider=document.getElementById('hider'+winState.id),
|
|
577
959
|
shower=()=>{
|
|
578
960
|
if(anim)wc.remove(anim);
|
|
579
|
-
|
|
961
|
+
winState.state='opened';
|
|
580
962
|
}
|
|
581
963
|
wEl.style.display='';
|
|
582
964
|
hider.remove();
|
|
583
965
|
if(anim){
|
|
584
966
|
wc.add(anim);
|
|
585
|
-
|
|
967
|
+
wEl.addEventListener('animationend',shower,this._ae);
|
|
586
968
|
}else
|
|
587
969
|
shower()
|
|
588
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(),
|
|
589
1017
|
};
|
|
590
|
-
_.wins={};
|
|
591
1018
|
|
|
592
|
-
|
|
593
|
-
_.$.on(window,'unhandledrejection',_.err.handleRejection);
|
|
594
|
-
_.$.on(window,'popstate',()=>{
|
|
1019
|
+
window.addEventListener('popstate',()=>{
|
|
595
1020
|
let l=_.link
|
|
1021
|
+
/*
|
|
1022
|
+
* popstate срабатывает когда:
|
|
1023
|
+
* - пользователь прыгает по истории назад/вперёд
|
|
1024
|
+
* - мы вызываем history.pushState (не replaceState)
|
|
1025
|
+
*
|
|
1026
|
+
* _i различает эти случаи:
|
|
1027
|
+
* true = пользователь прыгнул назад
|
|
1028
|
+
* false = страница пишет свой адрес в ссылку
|
|
1029
|
+
*/
|
|
1030
|
+
// ???: некоторые браузеры могут вызывать popstate и при реплейсе
|
|
596
1031
|
if (!l._i) {
|
|
1032
|
+
// здесь происходит перенос команд при popstate
|
|
1033
|
+
// читайте _.lang.get() если хотите узнать почему
|
|
597
1034
|
let nUrl='?' + [l.compile()[0],...l._cmd].join('&');
|
|
598
|
-
history.replaceState(null,null,nUrl);
|
|
599
1035
|
l._i=true;
|
|
1036
|
+
history.replaceState(null,null,nUrl);
|
|
600
1037
|
l.get();
|
|
601
1038
|
} else
|
|
602
1039
|
l._i=false;
|
|
603
1040
|
});
|
|
1041
|
+
|
|
1042
|
+
return _};
|