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