@brandup/ui-app 1.0.1
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 +116 -0
- package/dist/cjs/index.js +886 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/mjs/index.js +880 -0
- package/dist/mjs/index.js.map +1 -0
- package/dist/types.d.ts +226 -0
- package/package.json +50 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ElemAttributeName = "uiElement";
|
|
4
|
+
const ElemPropertyName = "brandupUiElement";
|
|
5
|
+
const CommandAttributeName = "command";
|
|
6
|
+
const CommandExecutingCssClassName = "executing";
|
|
7
|
+
class UIElement {
|
|
8
|
+
__element = null;
|
|
9
|
+
__events = {};
|
|
10
|
+
__commandHandlers = {};
|
|
11
|
+
// Element members
|
|
12
|
+
get element() { return this.__element; }
|
|
13
|
+
setElement(elem) {
|
|
14
|
+
if (!elem)
|
|
15
|
+
throw "Not set value elem.";
|
|
16
|
+
if (this.__element || UIElement.hasElement(elem))
|
|
17
|
+
throw "UIElement already defined";
|
|
18
|
+
this.__element = elem;
|
|
19
|
+
this.__element[ElemPropertyName] = this;
|
|
20
|
+
this.__element.dataset[ElemAttributeName] = this.typeName;
|
|
21
|
+
this.defineEvent("command", { cancelable: false, bubbles: true });
|
|
22
|
+
this._onRenderElement(elem);
|
|
23
|
+
}
|
|
24
|
+
// static members
|
|
25
|
+
static hasElement(elem) {
|
|
26
|
+
return !!elem.dataset[ElemAttributeName];
|
|
27
|
+
}
|
|
28
|
+
// HTMLElement event members
|
|
29
|
+
defineEvent(eventName, eventOptions) {
|
|
30
|
+
this.__events[eventName] = eventOptions ? eventOptions : null;
|
|
31
|
+
}
|
|
32
|
+
raiseEvent(eventName, eventArgs) {
|
|
33
|
+
if (!(eventName in this.__events))
|
|
34
|
+
throw new Error(`Not found event "${eventName}".`);
|
|
35
|
+
const eventOptions = this.__events[eventName];
|
|
36
|
+
const eventInit = {};
|
|
37
|
+
if (eventOptions) {
|
|
38
|
+
if (eventOptions.bubbles)
|
|
39
|
+
eventInit.bubbles = eventOptions.bubbles;
|
|
40
|
+
if (eventOptions.cancelable)
|
|
41
|
+
eventInit.cancelable = eventOptions.cancelable;
|
|
42
|
+
if (eventOptions.composed)
|
|
43
|
+
eventInit.composed = eventOptions.composed;
|
|
44
|
+
}
|
|
45
|
+
eventInit.detail = eventArgs ? eventArgs : {};
|
|
46
|
+
const event = new CustomEvent(eventName, eventInit);
|
|
47
|
+
return this.dispatchEvent(event);
|
|
48
|
+
}
|
|
49
|
+
addEventListener(type, listener, options) {
|
|
50
|
+
this.__element?.addEventListener(type, listener, options);
|
|
51
|
+
}
|
|
52
|
+
removeEventListener(type, listener, options) {
|
|
53
|
+
this.__element?.removeEventListener(type, listener, options);
|
|
54
|
+
}
|
|
55
|
+
dispatchEvent(event) {
|
|
56
|
+
if (!this.__element)
|
|
57
|
+
throw new Error("HTMLElement is not defined.");
|
|
58
|
+
return this.__element.dispatchEvent(event);
|
|
59
|
+
}
|
|
60
|
+
// Command members
|
|
61
|
+
registerCommand(name, execute, canExecute) {
|
|
62
|
+
name = this.verifyCommandName(name);
|
|
63
|
+
this.__commandHandlers[name] = {
|
|
64
|
+
name: name,
|
|
65
|
+
execute,
|
|
66
|
+
canExecute,
|
|
67
|
+
isExecuting: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
registerAsyncCommand(name, delegate, canExecute) {
|
|
71
|
+
name = this.verifyCommandName(name);
|
|
72
|
+
this.__commandHandlers[name] = {
|
|
73
|
+
name: name,
|
|
74
|
+
delegate,
|
|
75
|
+
canExecute,
|
|
76
|
+
isExecuting: false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
hasCommand(name) {
|
|
80
|
+
return name.toLowerCase() in this.__commandHandlers;
|
|
81
|
+
}
|
|
82
|
+
execCommand(name, elem) {
|
|
83
|
+
if (!this.__element)
|
|
84
|
+
throw new Error("UIElement is not set HTMLElement.");
|
|
85
|
+
const key = name.toLowerCase();
|
|
86
|
+
if (!(key in this.__commandHandlers))
|
|
87
|
+
throw new Error(`Command "${name}" is not registered.`);
|
|
88
|
+
const context = {
|
|
89
|
+
target: elem,
|
|
90
|
+
uiElem: this,
|
|
91
|
+
transparent: false
|
|
92
|
+
};
|
|
93
|
+
const handler = this.__commandHandlers[key];
|
|
94
|
+
if (handler.isExecuting)
|
|
95
|
+
return { result: CommandsExecStatus.AlreadyExecuting, context };
|
|
96
|
+
handler.isExecuting = true;
|
|
97
|
+
if (!this._onCanExecCommand(name, elem)) {
|
|
98
|
+
handler.isExecuting = false;
|
|
99
|
+
return { result: CommandsExecStatus.NotAllow, context };
|
|
100
|
+
}
|
|
101
|
+
if (handler.canExecute && !handler.canExecute(elem, context)) {
|
|
102
|
+
handler.isExecuting = false;
|
|
103
|
+
return { result: CommandsExecStatus.NotAllow, context };
|
|
104
|
+
}
|
|
105
|
+
this.raiseEvent("command", {
|
|
106
|
+
name: handler.name,
|
|
107
|
+
uiElem: this,
|
|
108
|
+
elem: this.__element
|
|
109
|
+
});
|
|
110
|
+
if (handler.execute) {
|
|
111
|
+
// Если команда синхронная.
|
|
112
|
+
try {
|
|
113
|
+
handler.execute(elem, context);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
handler.isExecuting = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (handler.delegate) {
|
|
120
|
+
// Если команда асинхронная.
|
|
121
|
+
elem.classList.add(CommandExecutingCssClassName);
|
|
122
|
+
let timeoutId = 0;
|
|
123
|
+
const endFunc = () => {
|
|
124
|
+
handler.isExecuting = false;
|
|
125
|
+
elem.classList.remove(CommandExecutingCssClassName);
|
|
126
|
+
};
|
|
127
|
+
const asyncContext = {
|
|
128
|
+
target: elem,
|
|
129
|
+
uiElem: this,
|
|
130
|
+
transparent: context.transparent,
|
|
131
|
+
complate: () => {
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
endFunc();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const handlerResult = handler.delegate(asyncContext);
|
|
137
|
+
if (handlerResult && handlerResult instanceof Promise)
|
|
138
|
+
handlerResult.finally(() => asyncContext.complate());
|
|
139
|
+
if (handler.isExecuting && asyncContext.timeout) {
|
|
140
|
+
timeoutId = window.setTimeout(() => {
|
|
141
|
+
if (asyncContext.timeoutCallback)
|
|
142
|
+
asyncContext.timeoutCallback();
|
|
143
|
+
endFunc();
|
|
144
|
+
}, asyncContext.timeout);
|
|
145
|
+
}
|
|
146
|
+
context.transparent = asyncContext.transparent;
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
throw new Error("Not set command execute flow.");
|
|
150
|
+
return { result: CommandsExecStatus.Success, context: context };
|
|
151
|
+
}
|
|
152
|
+
verifyCommandName(name) {
|
|
153
|
+
const key = name.toLowerCase();
|
|
154
|
+
if (key in this.__commandHandlers)
|
|
155
|
+
throw new Error(`Command "${name}" already registered.`);
|
|
156
|
+
return key;
|
|
157
|
+
}
|
|
158
|
+
_onRenderElement(_elem) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
_onCanExecCommand(_name, _elem) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
destroy() {
|
|
165
|
+
const elem = this.__element;
|
|
166
|
+
if (!elem)
|
|
167
|
+
return;
|
|
168
|
+
this.__element = null;
|
|
169
|
+
delete elem.dataset[ElemAttributeName];
|
|
170
|
+
delete elem[ElemPropertyName];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
var CommandsExecStatus;
|
|
174
|
+
(function (CommandsExecStatus) {
|
|
175
|
+
CommandsExecStatus[CommandsExecStatus["NotAllow"] = 1] = "NotAllow";
|
|
176
|
+
CommandsExecStatus[CommandsExecStatus["AlreadyExecuting"] = 2] = "AlreadyExecuting";
|
|
177
|
+
CommandsExecStatus[CommandsExecStatus["Success"] = 3] = "Success";
|
|
178
|
+
})(CommandsExecStatus || (CommandsExecStatus = {}));
|
|
179
|
+
const fundUiElementByCommand = (elem, commandName) => {
|
|
180
|
+
while (elem) {
|
|
181
|
+
if (elem.dataset[ElemAttributeName]) {
|
|
182
|
+
const uiElem = elem[ElemPropertyName];
|
|
183
|
+
if (uiElem.hasCommand(commandName))
|
|
184
|
+
return uiElem;
|
|
185
|
+
}
|
|
186
|
+
if (typeof elem.parentElement === "undefined")
|
|
187
|
+
elem = elem.parentNode;
|
|
188
|
+
else if (elem.parentElement)
|
|
189
|
+
elem = elem.parentElement;
|
|
190
|
+
else
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
};
|
|
195
|
+
const commandClickHandler = (e) => {
|
|
196
|
+
let commandElem = e.target;
|
|
197
|
+
while (commandElem) {
|
|
198
|
+
if (commandElem.dataset[CommandAttributeName])
|
|
199
|
+
break;
|
|
200
|
+
if (commandElem === e.currentTarget)
|
|
201
|
+
return;
|
|
202
|
+
commandElem = commandElem.parentElement;
|
|
203
|
+
}
|
|
204
|
+
if (!commandElem)
|
|
205
|
+
return;
|
|
206
|
+
const commandName = commandElem.dataset[CommandAttributeName];
|
|
207
|
+
if (!commandName)
|
|
208
|
+
throw new Error("Command data attribute is not have value.");
|
|
209
|
+
const uiElem = fundUiElementByCommand(commandElem, commandName);
|
|
210
|
+
if (uiElem === null) {
|
|
211
|
+
console.warn(`Not find handler for command "${commandName}".`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const commandResult = uiElem.execCommand(commandName, commandElem);
|
|
215
|
+
if (commandResult.context.transparent)
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
e.stopImmediatePropagation();
|
|
221
|
+
};
|
|
222
|
+
window.addEventListener("click", commandClickHandler, false);
|
|
223
|
+
|
|
224
|
+
class MiddlewareInvoker {
|
|
225
|
+
middleware;
|
|
226
|
+
__next = null;
|
|
227
|
+
constructor(middleware) {
|
|
228
|
+
this.middleware = middleware;
|
|
229
|
+
}
|
|
230
|
+
next(middleware) {
|
|
231
|
+
if (this.__next)
|
|
232
|
+
this.__next.next(middleware);
|
|
233
|
+
else
|
|
234
|
+
this.__next = new MiddlewareInvoker(middleware);
|
|
235
|
+
}
|
|
236
|
+
async invoke(method, context) {
|
|
237
|
+
try {
|
|
238
|
+
await this.__invoke(method, context);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
console.error(`Error middleware "${method}" execution: ${e}`);
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async __invoke(method, context) {
|
|
246
|
+
const nextFunc = () => { return this.__next ? this.__next.__invoke(method, context) : Promise.resolve(); };
|
|
247
|
+
const methodFunc = this.middleware[method];
|
|
248
|
+
if (typeof methodFunc === "function") {
|
|
249
|
+
const methodResult = methodFunc.call(this.middleware, context, nextFunc);
|
|
250
|
+
if (!methodResult || !(methodResult instanceof Promise))
|
|
251
|
+
throw new Error(`Middleware method "${method}" is not async.`);
|
|
252
|
+
await methodResult;
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
await nextFunc();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = {
|
|
260
|
+
LoadingElementClass: "loading",
|
|
261
|
+
NavUrlClassName: "applink",
|
|
262
|
+
FormClassName: "appform",
|
|
263
|
+
NavUrlAttributeName: "data-nav-url",
|
|
264
|
+
NavUrlReplaceAttributeName: "data-nav-replace",
|
|
265
|
+
NavIgnoreAttributeName: "data-nav-ignore",
|
|
266
|
+
STATE_CLASS: {
|
|
267
|
+
LOADING: "bp-state-loading",
|
|
268
|
+
LOADED: "bp-state-loaded",
|
|
269
|
+
READY: "bp-state-ready"
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
var constants = /*#__PURE__*/Object.freeze({
|
|
274
|
+
__proto__: null,
|
|
275
|
+
default: result
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const STATE_MIDDLEWARE_NAME = "app-state";
|
|
279
|
+
const StateMiddlewareFactory = () => {
|
|
280
|
+
let counter = 0;
|
|
281
|
+
const begin = (context) => {
|
|
282
|
+
counter++;
|
|
283
|
+
context.app.element?.classList.remove(result.STATE_CLASS.LOADED);
|
|
284
|
+
context.app.element?.classList.add(result.STATE_CLASS.LOADING);
|
|
285
|
+
};
|
|
286
|
+
const end = (context) => {
|
|
287
|
+
counter--;
|
|
288
|
+
if (counter <= 0) {
|
|
289
|
+
counter = 0;
|
|
290
|
+
context.app.element?.classList.remove(result.STATE_CLASS.LOADING);
|
|
291
|
+
context.app.element?.classList.add(result.STATE_CLASS.LOADED);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
return {
|
|
295
|
+
name: STATE_MIDDLEWARE_NAME,
|
|
296
|
+
start: async (context, next) => {
|
|
297
|
+
begin(context);
|
|
298
|
+
try {
|
|
299
|
+
await next();
|
|
300
|
+
}
|
|
301
|
+
catch (reason) {
|
|
302
|
+
end(context);
|
|
303
|
+
throw reason;
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
loaded: async (context, next) => {
|
|
307
|
+
try {
|
|
308
|
+
await next();
|
|
309
|
+
context.app.element?.classList.add(result.STATE_CLASS.READY);
|
|
310
|
+
}
|
|
311
|
+
catch (reason) {
|
|
312
|
+
end(context);
|
|
313
|
+
throw reason;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
navigate: async (context, next) => {
|
|
317
|
+
begin(context);
|
|
318
|
+
try {
|
|
319
|
+
await next();
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
if (context.source == "first")
|
|
323
|
+
end(context);
|
|
324
|
+
end(context);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
submit: async (context, next) => {
|
|
328
|
+
try {
|
|
329
|
+
await next();
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
end(context);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
stop: async (_context, next) => {
|
|
336
|
+
await next();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const win = window;
|
|
342
|
+
const doc = window.document;
|
|
343
|
+
const body = doc.body;
|
|
344
|
+
const loc = window.location;
|
|
345
|
+
var BROWSER = {
|
|
346
|
+
window: win,
|
|
347
|
+
document: doc,
|
|
348
|
+
body,
|
|
349
|
+
location: loc,
|
|
350
|
+
reload: () => loc.reload()
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
var browser = /*#__PURE__*/Object.freeze({
|
|
354
|
+
__proto__: null,
|
|
355
|
+
default: BROWSER
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const HYPERLINK_MIDDLEWARE_NAME = "app-hyperlink";
|
|
359
|
+
const HyperLinkMiddlewareFactory = () => {
|
|
360
|
+
let __ctrlPressed = false;
|
|
361
|
+
const onKeyDownUp = (e) => {
|
|
362
|
+
__ctrlPressed = e.ctrlKey;
|
|
363
|
+
};
|
|
364
|
+
let onClick;
|
|
365
|
+
return {
|
|
366
|
+
name: HYPERLINK_MIDDLEWARE_NAME,
|
|
367
|
+
start: async (context, next) => {
|
|
368
|
+
await next();
|
|
369
|
+
BROWSER.window.addEventListener("click", onClick = (e) => {
|
|
370
|
+
let elem = e.target;
|
|
371
|
+
let ignore = false;
|
|
372
|
+
while (elem) {
|
|
373
|
+
if (elem instanceof Element) {
|
|
374
|
+
if (elem.hasAttribute(result.NavIgnoreAttributeName)) {
|
|
375
|
+
ignore = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (elem.classList && elem.classList.contains(result.NavUrlClassName))
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
if (elem === e.currentTarget)
|
|
382
|
+
return;
|
|
383
|
+
elem = elem.parentElement;
|
|
384
|
+
}
|
|
385
|
+
if (!elem || __ctrlPressed || elem.getAttribute("target") === "_blank")
|
|
386
|
+
return;
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
e.stopPropagation();
|
|
389
|
+
if (ignore)
|
|
390
|
+
return;
|
|
391
|
+
let url;
|
|
392
|
+
if (elem.tagName === "A")
|
|
393
|
+
url = elem.getAttribute("href");
|
|
394
|
+
else if (elem.hasAttribute(result.NavUrlAttributeName))
|
|
395
|
+
url = elem.getAttribute(result.NavUrlAttributeName);
|
|
396
|
+
else
|
|
397
|
+
throw "Not found url for navigation.";
|
|
398
|
+
if (elem.classList.contains(result.LoadingElementClass))
|
|
399
|
+
return;
|
|
400
|
+
elem.classList.add(result.LoadingElementClass);
|
|
401
|
+
context.app
|
|
402
|
+
.nav({ url, replace: elem.hasAttribute(result.NavUrlReplaceAttributeName), data: { clickElem: elem } })
|
|
403
|
+
.finally(() => elem.classList.remove(result.LoadingElementClass));
|
|
404
|
+
}, false);
|
|
405
|
+
BROWSER.window.addEventListener("keydown", onKeyDownUp, false);
|
|
406
|
+
BROWSER.window.addEventListener("keyup", onKeyDownUp, false);
|
|
407
|
+
},
|
|
408
|
+
stop: (_context, next) => {
|
|
409
|
+
BROWSER.window.removeEventListener("click", onClick, false);
|
|
410
|
+
BROWSER.window.removeEventListener("keydown", onKeyDownUp, false);
|
|
411
|
+
BROWSER.window.removeEventListener("keyup", onKeyDownUp, false);
|
|
412
|
+
return next();
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const FORM_MIDDLEWARE_NAME = "app-form";
|
|
418
|
+
const FormMiddlewareFactory = () => {
|
|
419
|
+
let onWindowSubmit;
|
|
420
|
+
return {
|
|
421
|
+
name: FORM_MIDDLEWARE_NAME,
|
|
422
|
+
start: async (context, next) => {
|
|
423
|
+
await next();
|
|
424
|
+
BROWSER.window.addEventListener("submit", onWindowSubmit = (e) => {
|
|
425
|
+
const form = e.target;
|
|
426
|
+
if (!form.classList.contains(result.FormClassName))
|
|
427
|
+
return;
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
context.app.submit({ form, button: e.submitter instanceof HTMLButtonElement ? e.submitter : null });
|
|
430
|
+
}, false);
|
|
431
|
+
},
|
|
432
|
+
submit: async (context, next) => {
|
|
433
|
+
const { form, button, method } = context;
|
|
434
|
+
if (!form.checkValidity())
|
|
435
|
+
return Promise.reject('Form is invalid.');
|
|
436
|
+
if (form.classList.contains(result.LoadingElementClass))
|
|
437
|
+
return form["_submit_"];
|
|
438
|
+
form.classList.add(result.LoadingElementClass);
|
|
439
|
+
if (button)
|
|
440
|
+
button.classList.add(result.LoadingElementClass);
|
|
441
|
+
context.replace = form.hasAttribute(result.NavUrlReplaceAttributeName);
|
|
442
|
+
if (button && button.hasAttribute(result.NavUrlReplaceAttributeName))
|
|
443
|
+
context.replace = true;
|
|
444
|
+
try {
|
|
445
|
+
if (method === "GET")
|
|
446
|
+
await context.app.nav({ url: context.url, query: new FormData(form), data: context.data });
|
|
447
|
+
else
|
|
448
|
+
await next();
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
form.classList.remove(result.LoadingElementClass);
|
|
452
|
+
if (button)
|
|
453
|
+
button.classList.remove(result.LoadingElementClass);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
stop: (_context, next) => {
|
|
457
|
+
BROWSER.window.removeEventListener("submit", onWindowSubmit, false);
|
|
458
|
+
return next();
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const parseUrl = (url) => {
|
|
464
|
+
const loc = BROWSER.location;
|
|
465
|
+
let origin = loc.origin;
|
|
466
|
+
let path;
|
|
467
|
+
let query = null;
|
|
468
|
+
let hash = null;
|
|
469
|
+
let isExternal = false;
|
|
470
|
+
if (!url) {
|
|
471
|
+
path = loc.pathname;
|
|
472
|
+
if (loc.search)
|
|
473
|
+
query = new URLSearchParams(loc.search);
|
|
474
|
+
if (loc.hash)
|
|
475
|
+
hash = loc.hash;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
if (url.startsWith("#")) {
|
|
479
|
+
path = loc.pathname;
|
|
480
|
+
query = new URLSearchParams(loc.search);
|
|
481
|
+
hash = url;
|
|
482
|
+
}
|
|
483
|
+
else if (url.startsWith("?")) {
|
|
484
|
+
const hastIndex = url.lastIndexOf("#");
|
|
485
|
+
if (hastIndex !== -1) {
|
|
486
|
+
hash = url.substring(hastIndex);
|
|
487
|
+
url = url.substring(0, hastIndex);
|
|
488
|
+
}
|
|
489
|
+
path = loc.pathname;
|
|
490
|
+
query = new URLSearchParams(url);
|
|
491
|
+
}
|
|
492
|
+
else if (url.startsWith("http")) {
|
|
493
|
+
const u = new URL(url);
|
|
494
|
+
if (u.origin != origin) {
|
|
495
|
+
origin = u.origin;
|
|
496
|
+
isExternal = true;
|
|
497
|
+
}
|
|
498
|
+
path = u.pathname;
|
|
499
|
+
query = u.searchParams;
|
|
500
|
+
hash = u.hash || null;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
const hastIndex = url.lastIndexOf("#");
|
|
504
|
+
if (hastIndex !== -1) {
|
|
505
|
+
hash = url.substring(hastIndex);
|
|
506
|
+
url = url.substring(0, hastIndex);
|
|
507
|
+
}
|
|
508
|
+
const queryIndex = url.lastIndexOf("?");
|
|
509
|
+
if (queryIndex !== -1) {
|
|
510
|
+
query = new URLSearchParams(url.substring(queryIndex));
|
|
511
|
+
url = url.substring(0, queryIndex);
|
|
512
|
+
}
|
|
513
|
+
path = url;
|
|
514
|
+
if (!path.startsWith("/")) {
|
|
515
|
+
let curPath = loc.pathname;
|
|
516
|
+
if (curPath.endsWith("/"))
|
|
517
|
+
curPath = curPath.substring(0, curPath.length - 1);
|
|
518
|
+
path = curPath + "/" + path;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (!path)
|
|
523
|
+
path = "/";
|
|
524
|
+
else if (path.length > 1 && path.endsWith("/"))
|
|
525
|
+
path = path.substring(0, path.length - 1);
|
|
526
|
+
if (!query)
|
|
527
|
+
query = new URLSearchParams();
|
|
528
|
+
if (hash === "#")
|
|
529
|
+
hash = null;
|
|
530
|
+
else if (hash)
|
|
531
|
+
hash = hash.substring(1);
|
|
532
|
+
var result = {
|
|
533
|
+
full: "",
|
|
534
|
+
relative: "",
|
|
535
|
+
origin,
|
|
536
|
+
path,
|
|
537
|
+
query,
|
|
538
|
+
hash,
|
|
539
|
+
external: isExternal
|
|
540
|
+
};
|
|
541
|
+
rebuildUrl(result);
|
|
542
|
+
return result;
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Add or replace query parameters.
|
|
546
|
+
* @param url Source url for extending query.
|
|
547
|
+
* @param query New or update parameters.
|
|
548
|
+
*/
|
|
549
|
+
const extendQuery = (url, query) => {
|
|
550
|
+
if (query instanceof URLSearchParams) {
|
|
551
|
+
query.forEach((v, k) => url.query.delete(k));
|
|
552
|
+
query.forEach((v, k) => url.query.append(k, v));
|
|
553
|
+
}
|
|
554
|
+
else if (query instanceof FormData) {
|
|
555
|
+
query.forEach((v, k) => url.query.delete(k));
|
|
556
|
+
query.forEach((v, k) => url.query.append(k, v.toString()));
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
for (let key in query) {
|
|
560
|
+
const value = query[key];
|
|
561
|
+
if (!Array.isArray(value)) {
|
|
562
|
+
url.query.set(key, value);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
url.query.delete(key);
|
|
566
|
+
value.forEach(val => url.query.append(key, val));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
rebuildUrl(url);
|
|
571
|
+
};
|
|
572
|
+
const rebuildUrl = (url) => {
|
|
573
|
+
let relativeUrl = url.path;
|
|
574
|
+
if (url.query.size)
|
|
575
|
+
relativeUrl += "?" + url.query.toString();
|
|
576
|
+
if (url.hash)
|
|
577
|
+
relativeUrl += "#" + url.hash;
|
|
578
|
+
url.full = url.origin + relativeUrl;
|
|
579
|
+
url.relative = relativeUrl;
|
|
580
|
+
};
|
|
581
|
+
const buildUrl = (basePath, path, query, hash) => {
|
|
582
|
+
let url = basePath;
|
|
583
|
+
if (path) {
|
|
584
|
+
if (path.startsWith("/"))
|
|
585
|
+
path = path.substring(1);
|
|
586
|
+
url += path;
|
|
587
|
+
}
|
|
588
|
+
if (query) {
|
|
589
|
+
let params;
|
|
590
|
+
if (query instanceof URLSearchParams)
|
|
591
|
+
params = query;
|
|
592
|
+
else if (query instanceof FormData) {
|
|
593
|
+
params = new URLSearchParams();
|
|
594
|
+
query.forEach((value, key) => params.append(key, value.toString()));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
params = new URLSearchParams();
|
|
598
|
+
for (const key in query) {
|
|
599
|
+
const value = query[key];
|
|
600
|
+
if (value === null || typeof value === "undefined")
|
|
601
|
+
continue;
|
|
602
|
+
if (Array.isArray(value))
|
|
603
|
+
value.forEach(v => params.append(key, v));
|
|
604
|
+
else
|
|
605
|
+
params.append(key, value);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (params.size)
|
|
609
|
+
url += "?" + params.toString();
|
|
610
|
+
}
|
|
611
|
+
if (hash) {
|
|
612
|
+
if (!hash.startsWith("#"))
|
|
613
|
+
hash = "#" + hash;
|
|
614
|
+
if (hash != "#")
|
|
615
|
+
url += hash;
|
|
616
|
+
}
|
|
617
|
+
return url;
|
|
618
|
+
};
|
|
619
|
+
var urlHelper = {
|
|
620
|
+
parseUrl,
|
|
621
|
+
extendQuery,
|
|
622
|
+
buildUrl
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
var url = /*#__PURE__*/Object.freeze({
|
|
626
|
+
__proto__: null,
|
|
627
|
+
default: urlHelper
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Base application class.
|
|
632
|
+
*/
|
|
633
|
+
class Application extends UIElement {
|
|
634
|
+
env;
|
|
635
|
+
model;
|
|
636
|
+
__invoker;
|
|
637
|
+
__isInitialized = false;
|
|
638
|
+
__isDestroy = false;
|
|
639
|
+
__middlewares = {};
|
|
640
|
+
__lastNav = null;
|
|
641
|
+
constructor(env, model) {
|
|
642
|
+
super();
|
|
643
|
+
this.env = env;
|
|
644
|
+
this.model = model;
|
|
645
|
+
const core = { name: "app-root" };
|
|
646
|
+
this.__invoker = new MiddlewareInvoker(core);
|
|
647
|
+
}
|
|
648
|
+
get typeName() { return "Application"; }
|
|
649
|
+
/** Middleware methods invoker. */
|
|
650
|
+
get invoker() { return this.__invoker; }
|
|
651
|
+
/**
|
|
652
|
+
* @param middlewares Initialize application with middlewares. Using in ApplicationBuilder.
|
|
653
|
+
*/
|
|
654
|
+
initialize(middlewares) {
|
|
655
|
+
if (this.__isInitialized)
|
|
656
|
+
throw 'Application already initialized.';
|
|
657
|
+
this.__isInitialized = true;
|
|
658
|
+
this.onInitialize();
|
|
659
|
+
middlewares.forEach(middleware => {
|
|
660
|
+
var name = middleware.name;
|
|
661
|
+
if (this.__middlewares.hasOwnProperty(name))
|
|
662
|
+
throw `Middleware "${name}" already registered.`;
|
|
663
|
+
this.__middlewares[name] = middleware;
|
|
664
|
+
this.__invoker.next(middleware);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
onInitialize() {
|
|
668
|
+
this.__invoker.next(StateMiddlewareFactory());
|
|
669
|
+
this.__invoker.next(HyperLinkMiddlewareFactory());
|
|
670
|
+
this.__invoker.next(FormMiddlewareFactory());
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get middleware by type.
|
|
674
|
+
* @param type Type of middleware.
|
|
675
|
+
* @returns Middleware instance.
|
|
676
|
+
*/
|
|
677
|
+
middleware(name) {
|
|
678
|
+
const middleware = this.__middlewares[name];
|
|
679
|
+
if (!middleware)
|
|
680
|
+
throw `Middleware ${name} is not registered.`;
|
|
681
|
+
return middleware;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Run application.
|
|
685
|
+
* @param contextData Run context data.
|
|
686
|
+
* @param element HTMLElement of application. Default is document.body.
|
|
687
|
+
* @returns Promise of runned result.
|
|
688
|
+
*/
|
|
689
|
+
async run(contextData, element) {
|
|
690
|
+
if (!contextData)
|
|
691
|
+
contextData = {};
|
|
692
|
+
this.setElement(element || BROWSER.body);
|
|
693
|
+
try {
|
|
694
|
+
const context = {
|
|
695
|
+
app: this,
|
|
696
|
+
data: contextData
|
|
697
|
+
};
|
|
698
|
+
await this.__invoker.invoke("start", context);
|
|
699
|
+
console.info("app start success");
|
|
700
|
+
await this.__invoker.invoke("loaded", context);
|
|
701
|
+
console.info("app load success");
|
|
702
|
+
return await this.nav({ data: context.data });
|
|
703
|
+
}
|
|
704
|
+
catch (reason) {
|
|
705
|
+
console.error(`Unable to run application with reason: ${reason}`);
|
|
706
|
+
throw reason;
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
console.info("app runned");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Navigate application to url.
|
|
714
|
+
* @param options Navigate options.
|
|
715
|
+
* @returns Promise of navigated result.
|
|
716
|
+
*/
|
|
717
|
+
async nav(options) {
|
|
718
|
+
const opt = (!options || options instanceof String) ? { url: options } : options;
|
|
719
|
+
let { url = null, replace = false, data = {}, callback } = opt;
|
|
720
|
+
const navUrl = urlHelper.parseUrl(url);
|
|
721
|
+
if (opt.query)
|
|
722
|
+
urlHelper.extendQuery(navUrl, opt.query);
|
|
723
|
+
const isFirst = !this.__lastNav;
|
|
724
|
+
const context = this.__lastNav = {
|
|
725
|
+
app: this,
|
|
726
|
+
source: isFirst ? "first" : "nav",
|
|
727
|
+
data,
|
|
728
|
+
url: navUrl.full,
|
|
729
|
+
origin: navUrl.origin,
|
|
730
|
+
path: navUrl.path,
|
|
731
|
+
query: navUrl.query,
|
|
732
|
+
hash: navUrl.hash,
|
|
733
|
+
replace,
|
|
734
|
+
external: navUrl.external
|
|
735
|
+
};
|
|
736
|
+
console.info(context);
|
|
737
|
+
try {
|
|
738
|
+
console.info(`app nav begin ${navUrl.full}`);
|
|
739
|
+
await this.__invoker.invoke("navigate", context);
|
|
740
|
+
if (callback)
|
|
741
|
+
callback({ status: "success", context: context });
|
|
742
|
+
console.info(`app nav success ${navUrl.full}`);
|
|
743
|
+
return context;
|
|
744
|
+
}
|
|
745
|
+
catch (reason) {
|
|
746
|
+
if (callback)
|
|
747
|
+
callback({ status: "error", context: context });
|
|
748
|
+
console.error(`app nav error ${navUrl.full}: ${reason}`);
|
|
749
|
+
throw reason;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Submit application form.
|
|
754
|
+
* @param options Submit options.
|
|
755
|
+
* @returns Promise of submitted result.
|
|
756
|
+
*/
|
|
757
|
+
async submit(options) {
|
|
758
|
+
const opt = options instanceof HTMLFormElement ? { form: options } : options;
|
|
759
|
+
const { form, button = null, query, data = {}, callback = null } = opt;
|
|
760
|
+
let method = form.method;
|
|
761
|
+
let enctype = form.enctype;
|
|
762
|
+
let url = form.action;
|
|
763
|
+
if (button) {
|
|
764
|
+
// Get button patameters for request
|
|
765
|
+
if (button.hasAttribute("formmethod"))
|
|
766
|
+
method = button.formMethod;
|
|
767
|
+
if (button.hasAttribute("formenctype"))
|
|
768
|
+
enctype = button.formEnctype;
|
|
769
|
+
if (button.hasAttribute("formaction"))
|
|
770
|
+
url = button.formAction;
|
|
771
|
+
}
|
|
772
|
+
method = method.toUpperCase();
|
|
773
|
+
const navUrl = urlHelper.parseUrl(url);
|
|
774
|
+
if (query)
|
|
775
|
+
urlHelper.extendQuery(navUrl, query);
|
|
776
|
+
let submitContext = {
|
|
777
|
+
app: this,
|
|
778
|
+
source: "submit",
|
|
779
|
+
data,
|
|
780
|
+
form,
|
|
781
|
+
button,
|
|
782
|
+
method,
|
|
783
|
+
enctype,
|
|
784
|
+
url: navUrl.full,
|
|
785
|
+
origin: navUrl.origin,
|
|
786
|
+
path: navUrl.path,
|
|
787
|
+
query: navUrl.query,
|
|
788
|
+
hash: navUrl.hash,
|
|
789
|
+
replace: false,
|
|
790
|
+
external: navUrl.external
|
|
791
|
+
};
|
|
792
|
+
try {
|
|
793
|
+
console.info(`submit ${method} begin ${navUrl.full}`);
|
|
794
|
+
await this.__invoker.invoke("submit", submitContext);
|
|
795
|
+
console.info(`submit ${method} success ${navUrl.full}`);
|
|
796
|
+
if (callback)
|
|
797
|
+
callback({ status: "success", context: submitContext });
|
|
798
|
+
}
|
|
799
|
+
catch (reason) {
|
|
800
|
+
console.error(`submit ${method} error ${navUrl.full} reason: ${reason}`);
|
|
801
|
+
if (callback)
|
|
802
|
+
callback({ status: "error", context: submitContext });
|
|
803
|
+
throw reason;
|
|
804
|
+
}
|
|
805
|
+
return submitContext;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Reload page with nav.
|
|
809
|
+
*/
|
|
810
|
+
reload() {
|
|
811
|
+
return this.nav({ replace: true });
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Global reload page in browser.
|
|
815
|
+
*/
|
|
816
|
+
restart() {
|
|
817
|
+
BROWSER.reload();
|
|
818
|
+
}
|
|
819
|
+
async destroy(contextData) {
|
|
820
|
+
if (this.__isDestroy)
|
|
821
|
+
return Promise.reject('Application already destroyed.');
|
|
822
|
+
this.__isDestroy = true;
|
|
823
|
+
console.info("app destroy begin");
|
|
824
|
+
const context = {
|
|
825
|
+
app: this,
|
|
826
|
+
data: contextData || {}
|
|
827
|
+
};
|
|
828
|
+
try {
|
|
829
|
+
await this.__invoker.invoke("stop", context);
|
|
830
|
+
console.info("app destroy success");
|
|
831
|
+
return context;
|
|
832
|
+
}
|
|
833
|
+
catch (reason) {
|
|
834
|
+
console.error(`app destroy error: ${reason}`);
|
|
835
|
+
throw reason;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Generate url of application base url.
|
|
840
|
+
* @param path Add optional path of base url.
|
|
841
|
+
* @param query Add optional query params.
|
|
842
|
+
* @param hash Add optional hash.
|
|
843
|
+
* @returns Relative url with base path.
|
|
844
|
+
*/
|
|
845
|
+
buildUrl(path, query, hash) {
|
|
846
|
+
return urlHelper.buildUrl(this.env.basePath, path, query, hash);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
class ApplicationBuilder {
|
|
851
|
+
__appType = (Application);
|
|
852
|
+
__middlewares = [];
|
|
853
|
+
useApp(appType) {
|
|
854
|
+
this.__appType = appType;
|
|
855
|
+
return this;
|
|
856
|
+
}
|
|
857
|
+
useMiddleware(middleware, ...params) {
|
|
858
|
+
if (!middleware)
|
|
859
|
+
throw `Middleware propery is required.`;
|
|
860
|
+
let midl;
|
|
861
|
+
if (typeof middleware === "function")
|
|
862
|
+
midl = middleware(...params);
|
|
863
|
+
else
|
|
864
|
+
midl = middleware;
|
|
865
|
+
this.__middlewares.push(midl);
|
|
866
|
+
return this;
|
|
867
|
+
}
|
|
868
|
+
build(env, model) {
|
|
869
|
+
if (!env)
|
|
870
|
+
throw new Error("Parameter env is required.");
|
|
871
|
+
if (!model)
|
|
872
|
+
throw new Error("Parameter model is required.");
|
|
873
|
+
if (!env.basePath)
|
|
874
|
+
env.basePath = "/";
|
|
875
|
+
const app = new this.__appType(env, model);
|
|
876
|
+
app.initialize(this.__middlewares);
|
|
877
|
+
return app;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
exports.Application = Application;
|
|
882
|
+
exports.ApplicationBuilder = ApplicationBuilder;
|
|
883
|
+
exports.BROWSER = browser;
|
|
884
|
+
exports.CONSTANTS = constants;
|
|
885
|
+
exports.UrlHelper = url;
|
|
886
|
+
//# sourceMappingURL=index.js.map
|